spec-kit 워크플로우

|

spec-kit 워크플로우

spec-kit은 무작위 프롬프트 대신 구조화된 개발 흐름을 제공함
₩constitution → specify → plan → tasks → implement₩ 순서로 진행하며, 이를 통해 체계적으로 코드 전환이 가능함

speci-kit의 개발 워크플로우는 아래의 5가지 단계로 진행

명령어 설명
/speckit-constitution 프로젝트 원칙 수립
/speckit-specify 요구사항 정의
/speckit-plan 계획 수립
/speckit-tasks 작업 분해
/speckit-implement 구현

0. claude 실행

아래 명령어들은 claude CLI 상태에서 호출하는 명령어임
speckit init도 이루어져야 함. 그래야 claude에서 명령어가 생성됨

1. Constitution (프로젝트 원칙 수립)

프로젝트 전체를 관통하는 개발 원칙과 철학을 정의

❯ /speckit-constitution 이 프로젝트는 개인 작업 관리를 위한 웹 기반 Todo 애플리케이션입니다.

핵심 원칙:
- UI는 단순하고 직관적으로 유지 — 불필요한 복잡성 배제
- 직접 구현보다 검증된 라이브러리를 우선 사용
- 백엔드 없이 localStorage로 데이터 저장
- 모바일 우선 반응형 디자인
- 로그인이나 인증 없이 모든 기능 사용 가능

위 명령어를 입력하면 claude가 자세한 문서를 생성해 줌

alt text

alt text

모든 것을 승인하면 아래와 같은 메시지 나오면서 문서 작성 완료

결과 화면

Constitution 작성 완료

버전: 1.0.0 (최초 제정) 버전 증가 근거: 템플릿에서 최초로 구체적인 내용을 채워 넣은 최초 제정(1.0.0)

확정된 5가지 원칙
# 원칙명 핵심 규칙
I 단순성 우선 불필요한 UI 요소·옵션 금지. 복잡성 도입 시 plan.md에 정당화 기록 필수
II 라이브러리 우선 공통 기능은 반드시 기존 라이브러리 탐색 후 채택. 직접 구현 시 사유 기록 필수
III LocalStorage-Only 외부 서버·API 금지. JSON 스키마 버전 필드 포함 권장
IV 모바일 우선 반응형 320px 기준 설계, 터치 타겟 44px 이상, min-width 확장
V 인증 불필요 로그인/OAuth/세션 로직 일절 금지
템플릿 검토 결과
  • .specify/templates/plan-template.md ✅ Constitution Check 섹션 이미 존재
  • .specify/templates/spec-template.md ✅ 인증 요구사항 없음 — 충돌 없음
  • .specify/templates/tasks-template.md ✅ 백엔드/인증 태스크 카테고리 불필요
  • .specify/templates/commands/ — 디렉토리 없음, 스킵
권장 커밋 메시지

docs: ratify constitution v1.0.0 (Todo App — simplicity, library-first, localStorage, mobile-first, auth-free)

2. Specify (요구사항 정의)

/speckit-specify

다음 기능을 포함하는 웹 기반 Todo 애플리케이션을 만들어주세요:

- 텍스트 입력으로 새 할 일 추가
- 체크박스로 완료/미완료 토글
- 개별 할 일 삭제
- 상태별 필터링: 전체 / 진행 중 / 완료됨
- 남은 할 일 개수 표시
- 페이지 새로고침 후에도 유지되도록 localStorage에 저장
- 완료된 항목 전체 삭제 기능

기술 스택: HTML, 순수 CSS, 순수 JavaScript (프레임워크 없음)

그러면 아래와 같은 결과 화면에 터미널에 출력됨

결과 화면

완료 보고

Feature Directory: specs/001-todo-app Spec File: specs/001-todo-app/spec.md Checklist: specs/001-todo-app/checklists/requirements.md

체크리스트 결과: 전체 통과 ✅
항목 결과
Content Quality (4개) ✅ 모두 통과
Requirement Completeness (8개) ✅ 모두 통과
Feature Readiness (4개) ✅ 모두 통과
[NEEDS CLARIFICATION] 마커 없음
정의된 스토리 요약
우선순위 스토리 핵심 가치
P1 할 일 추가 및 관리 추가·완료 토글·삭제 + 남은 개수 표시
P2 상태별 필터링 전체 / 진행 중 / 완료됨 필터
P3 일괄 삭제 & 데이터 영속성 완료 항목 전체 삭제 + localStorage 저장
주요 결정사항
  • Constitution 예외: 사용자가 순수 JS를 명시적으로 요청 → 라이브러리 우선 원칙의 예외로 Assumptions에 기록. /speckit-plan 실행 시 plan.md Complexity Tracking에 정당화 필요.
  • 범위 외: 항목 수정(edit) 기능, 데이터 내보내기/가져오기, 다중 사용자/동기화

다음 단계: /speckit-plan 으로 구현 계획을 수립하거나, /speckit-clarify 로 추가 요구사항을 명확히 할 수 있습니다.

3. Plan (계획 수립)

/speckit-plan

요구사항을 바탕으로 Todo 앱의 기술 계획을 작성해주세요.

다음 내용을 포함해주세요:
- 파일 구조 (HTML / CSS / JS)
- Todo 항목의 데이터 모델 (id, text, completed, createdAt)
- localStorage 읽기/쓰기 처리 방식
- 컴포넌트 구분: 입력 영역, 목록, 필터 바, 푸터
- 이벤트 처리 전략 (추가, 토글, 삭제, 필터)

결과 화면

완료 보고

Branch: 001-todo-app Plan File: specs/001-todo-app/plan.md

생성된 아티팩트
파일 설명
specs/001-todo-app/plan.md 메인 구현 계획 (이 파일)
specs/001-todo-app/research.md Phase 0 — 7가지 기술 결정 문서
specs/001-todo-app/data-model.md Phase 1 — TodoItem / AppState 엔티티 정의
specs/001-todo-app/contracts/storage-schema.md localStorage 계약, UI 이벤트 계약
specs/001-todo-app/quickstart.md 8개 검증 시나리오 포함 실행 가이드
CLAUDE.md 에이전트 컨텍스트 → plan.md 경로로 업데이트
Constitution Check 결과
  • ✅ 원칙 I, III, IV, V — 완전 준수
  • ⚠️ 원칙 II — 사용자 명시 요청으로 예외 처리, Complexity Tracking에 기록됨
핵심 설계 결정
  • 3파일 구조: index.html + style.css + app.js (빌드 없음)
  • IIFE 패턴: 전역 스코프 오염 없이 앱 캡슐화
  • 이벤트 위임: 목록 컨테이너에 단일 리스너, data-id로 분기
  • 전체 재렌더: 상태 변경 시 innerHTML 교체 (규모 대비 충분)
  • localStorage 스키마: todo-app-v1 키, schemaVersion: 1 포함

다음 단계: /speckit-tasks 로 구현 태스크 목록을 생성할 수 있습니다.

4. Tasks (작업 분해)

/speckit-tasks

계획을 작고 실행 가능한 태스크 단위로 분해해주세요.
의존성 순서대로 정렬해주세요 (먼저 해야 할 것부터).

태스크 형식 예시:
- [ ] 기본 구조의 index.html 생성
- [ ] CSS 리셋 및 기본 스타일 추가
- [ ] localStorage 연동 TodoStore 클래스 구현
- [ ] 상태에서 할 일 목록 렌더링
- [ ] 엔터 키로 할 일 추가 처리
- [ ] 완료 토글 처리
- [ ] 할 일 삭제 처리
- [ ] 필터 구현 (전체 / 진행 중 / 완료됨)
- [ ] 푸터에 남은 개수 표시
- [ ] "완료 항목 삭제" 버튼 로직 추가

결과 화면

완료 보고

Tasks File: specs/001-todo-app/tasks.md

태스크 요약
페이즈 태스크 수 내용
Phase 1: Setup 1 디렉토리 생성
Phase 2: Foundational 6 HTML·CSS·JS 기반, localStorage I/O
Phase 3: US1 (P1) 15 추가·토글·삭제·렌더링·이벤트
Phase 4: US2 (P2) 4 필터링 로직 + CSS
Phase 5: US3 (P3) 4 일괄 삭제 + 영속성 검증
Phase 6: Polish 4 반응형·빈 상태·수동 검증
합계 34  
스토리별 독립 테스트 기준
스토리 검증 시나리오 독립 완료 조건
US1 (P1) quickstart.md S1–S4 추가·토글·삭제·카운트 동작
US2 (P2) quickstart.md S5 필터 3가지 전환 동작
US3 (P3) quickstart.md S6–S7 완료 삭제 + 새로고침 복원
병렬 실행 가능한 태스크
  • Foundational: T002+T003, T005+T006+T007 (3개씩 병렬)
  • US1 CSS: T010+T011+T012
  • US1 JS Mutations: T013+T014+T015
  • US2와 US3 전체 페이즈: US1 완료 후 동시 진행 가능
MVP 권장 범위

Phase 1 + Phase 2 + Phase 3 (T001–T022) — US1만으로 이미 완전한 기능을 갖춘 데모 가능.

다음 단계: /speckit-implement 으로 실제 구현을 시작할 수 있습니다.

5. Implement (구현)

/speckit-implement

태스크 목록의 모든 항목을 코드로 구현해주세요.
원칙(constitution)과 요구사항(spec)을 엄격히 따라주세요.

구현 가이드라인:
- 시맨틱 HTML5 요소 사용
- JS는 단일 app.js 파일로 유지
- 외부 JS 라이브러리 사용 금지
- 상태 변경 시마다 localStorage에 즉시 저장
- 완료 전 필터링 기능 정상 동작 확인입력 내용: /speckit-constitution

결과 화면

완료 보고

구현 완료: todo-app/ — 3개 파일, 총 ~1,400줄(HTML 1.4KB / CSS 6.4KB / JS 6.1KB)

완료된 태스크: 34/34 ✅

페이즈 태스크 상태
Phase 1: Setup T001
Phase 2: Foundational T002–T007
Phase 3: US1 (P1) — 추가·토글·삭제·카운트 T008–T022
Phase 4: US2 (P2) — 필터링 T023–T026
Phase 5: US3 (P3) — 완료 삭제·영속성 T027–T030
Phase 6: Polish T031–T034
구현 하이라이트
- index.html: 시맨틱 HTML5 (<main>, <header>, <footer>, <nav>), 접근성 aria-label + visually-hidden 레이블, defer 스크립트
- style.css: CSS 변수 기반 팔레트, 모바일 우선(320px), 터치 타겟 min-height: 44px, @media (min-width: 768px) 데스크톱 확장
- app.js: IIFE 캡슐화, escapeHtml() XSS 방어, crypto.randomUUID() + 폴백 ID 생성, localStorage try-catch 조용한 실패, 이벤트 위임 패턴
Constitution 준수
원칙 결과
I. 단순성 우선 ✅ 3파일, 외부 의존성 0
II. 라이브러리 우선 ⚠️ 예외(사용자 요청) — plan.md 기록됨
III. LocalStorage-Only ✅ todo-app-v1 키, 스키마 버전 1
IV. 모바일 우선 ✅ 320px 기준, 44px 터치 타겟
V. 인증 불필요 ✅ 즉시 사용 가능

브라우저에서 앱이 열렸습니다. quickstart.md S1–S8 시나리오로 수동 검증을 진행해 주세요.

최종 결과

alt text

spec-kit 설치 및 사용법

|

GitHub spec-kit

GitHub에서 만든 AI 기반 코딩을 위한 명세(Specification) 기반 개발을 할 수 있게 해주는 도구

설치 방법

# uv 설치
$ curl -LsSf https://astral.sh/uv/install.sh | sh

# spec-kit 설치
$ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git

# 설치 확인
$ specify --version

specify 0.10.3.dev0

# 시스템 요구사항 체크
$ specify check

GitHub Spec Kit - Spec-Driven Development Toolkit

Checking for installed tools...

Check Available Tools
├── ● Antigravity (available)
├── ● Amp (not found)
├── ● Auggie CLI (not found)
├── ○ IBM Bob (IDE-based, no CLI check)
├── ● Claude Code (available)
├── ○ Cline (IDE-based, no CLI check)
├── ● CodeBuddy (not found)
├── ● Codex CLI (not found)
├── ○ GitHub Copilot (IDE-based, no CLI check)
├── ○ Cursor (IDE-based, no CLI check)
├── ● Devin for Terminal (not found)
├── ● Forge (not found)
├── ● Gemini CLI (not found)
├── ● Goose (not found)
├── ● Hermes Agent (not found)
├── ● iFlow CLI (not found)
├── ● Junie (not found)
├── ○ Kilo Code (IDE-based, no CLI check)
├── ● Kimi Code (not found)
├── ● Kiro CLI (not found)
├── ○ Lingma (IDE-based, no CLI check)
├── ● opencode (not found)
├── ● Pi Coding Agent (not found)
├── ● Qoder CLI (not found)
├── ● Qwen Code (not found)
├── ○ Roo Code (IDE-based, no CLI check)
├── ● RovoDev ACLI (not found)
├── ● SHAI (not found)
├── ● Tabnine CLI (not found)
├── ○ Trae (IDE-based, no CLI check)
├── ● Mistral Vibe (not found)
├── ○ Windsurf (IDE-based, no CLI check)
├── ● Visual Studio Code (available)
└── ● Visual Studio Code Insiders (not found)

Specify CLI is ready to use!

프로젝트 시작하기

$ specify init snowdeer-sample-project

alt text

AI 모델을 선택할 수 있음
여기서는 claude 선택

그 이후 sh (POSIX Shell (bash/zsh)) 선택

alt text

그 이후 해당 디렉토리에 가면 아래와 같은 문서들이 생긴 것을 확인할 수 있음

.
├── .claude
│   └── skills
│       ├── speckit-agent-context-update
│       │   └── SKILL.md
│       ├── speckit-analyze
│       │   └── SKILL.md
│       ├── speckit-checklist
│       │   └── SKILL.md
│       ├── speckit-clarify
│       │   └── SKILL.md
│       ├── speckit-constitution
│       │   └── SKILL.md
│       ├── speckit-implement
│       │   └── SKILL.md
│       ├── speckit-plan
│       │   └── SKILL.md
│       ├── speckit-specify
│       │   └── SKILL.md
│       ├── speckit-tasks
│       │   └── SKILL.md
│       └── speckit-taskstoissues
│           └── SKILL.md
├── .specify
│   ├── extensions
│   │   ├── .registry
│   │   └── agent-context
│   │       ├── agent-context-config.yml
│   │       ├── commands
│   │       │   └── speckit.agent-context.update.md
│   │       ├── extension.yml
│   │       ├── README.md
│   │       └── scripts
│   │           ├── bash
│   │           │   └── update-agent-context.sh
│   │           └── powershell
│   │               └── update-agent-context.ps1
│   ├── extensions.yml
│   ├── init-options.json
│   ├── integration.json
│   ├── integrations
│   │   ├── claude.manifest.json
│   │   └── speckit.manifest.json
│   ├── memory
│   │   └── constitution.md
│   ├── scripts
│   │   └── bash
│   │       ├── check-prerequisites.sh
│   │       ├── common.sh
│   │       ├── create-new-feature.sh
│   │       ├── setup-plan.sh
│   │       └── setup-tasks.sh
│   ├── templates
│   │   ├── checklist-template.md
│   │   ├── constitution-template.md
│   │   ├── plan-template.md
│   │   ├── spec-template.md
│   │   └── tasks-template.md
│   └── workflows
│       ├── speckit
│       │   └── workflow.yml
│       └── workflow-registry.json
└── CLAUDE.md

또는 해당 디렉토리 들어가서 아래 명령어로 시작

specify init .

CLAUDE.md 예제 (Vue3)

|

CLAUDE.md 예제 (Vue3)

# CLAUDE.md

이 파일은 Claude가 이 프로젝트에서 코드를 생성하거나 수정할 때 참조하는 가이드입니다.

---

## 프로젝트 개요

- **프레임워크**: Vue 3 (Composition API)
- **언어**: TypeScript
- **빌드 도구**: Vite
- **상태 관리**: Pinia
- **라우터**: Vue Router 4
- **HTTP 클라이언트**: Axios (또는 ofetch)
- **구조**: 기능(feature)별 모듈 분리

---

## 프로젝트 구조

```
src/
├── main.ts                        # 앱 진입점
├── App.vue                        # 루트 컴포넌트
├── router/
│   └── index.ts                   # Vue Router 설정
├── stores/                        # Pinia 스토어 (기능별로 파일 분리)
│   ├── auth.ts                    # 예: 인증 스토어
│   ├── dashboard.ts               # 예: 대시보드 스토어
│   └── settings/
│       ├── wifi.ts                # 예: Wi-Fi 설정 스토어
│       └── audio.ts               # 예: 오디오 설정 스토어
├── network/
│   └── api/
│       ├── client.ts              # Axios 인스턴스 및 인터셉터
│       ├── dashboard.ts           # 대시보드 관련 API 함수
│       └── settings/
│           ├── wifi.ts            # Wi-Fi 설정 API 함수
│           └── audio.ts           # 오디오 설정 API 함수
├── views/                         # 라우트에 대응하는 페이지 컴포넌트
│   ├── HomeView.vue
│   ├── dashboard/
│   │   └── DashboardView.vue
│   └── settings/
│       ├── WifiSettingView.vue
│       └── AudioSettingView.vue
├── components/                    # 전역 공용 컴포넌트
│   └── BaseButton.vue
└── types/
    └── index.ts                   # 전역 공용 타입
```

### 구조 원칙

- **`views/`** — 라우트 1개에 View 컴포넌트 1개. 페이지 레이아웃만 담당하고, 비즈니스 로직은 스토어에 위임한다.
- **`stores/`** — 화면 구조와 동일한 디렉토리 계층으로 관리한다. 스토어가 `network/api/`를 직접 호출하며, View는 스토어만 바라본다.
- **`network/api/`** — 순수 HTTP 호출 함수만 둔다. 상태를 갖지 않으며, 스토어 외부에서 직접 호출하지 않는다.
- **`components/`** — 여러 View에서 공통으로 쓰는 UI 컴포넌트만 둔다. 특정 View 전용 컴포넌트는 `views/<domain>/components/`에 둔다.

---

## 주요 명령어

```bash
# 의존성 설치
npm install

# 개발 서버 실행
npm run dev

# 프로덕션 빌드
npm run build

# 타입 체크
npm run type-check

# 린트
npm run lint

# 프리뷰 (빌드 결과 확인)
npm run preview
```

---

## 코드 규칙

### 공통 원칙

- 모든 파일은 **TypeScript**로 작성한다. `.js`, `.jsx` 파일을 생성하지 않는다.
- `any` 타입을 사용하지 않는다. 불가피한 경우 `unknown`을 쓰고 타입 가드를 적용한다.
- `as` 타입 단언은 최소화한다. 가능하면 타입 가드 또는 제네릭으로 대체한다.
- 타입은 `interface`를 우선 사용하고, 유니온·교차 타입 등 표현식이 필요한 경우만 `type`을 사용한다.

---

### Vue 컴포넌트

- **Composition API + `<script setup>`** 방식만 사용한다. Options API는 쓰지 않는다.
- `defineProps`, `defineEmits`에는 반드시 TypeScript 제네릭 타입을 명시한다.
- Props는 가능하면 `withDefaults`로 기본값을 정의한다.
- 컴포넌트 파일명은 **PascalCase**로 작성한다. (예: `UserProfileCard.vue`)
- 한 컴포넌트 파일이 300줄을 넘으면 분리를 고려한다.
- 템플릿에서 복잡한 로직은 `computed`로 분리하거나 스토어에 위임한다.

```vue
<!-- 컴포넌트 예시 -->
<script setup lang="ts">
import { computed } from "vue";

interface Props {
  userId: number;
  isActive?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  isActive: false,
});

const emit = defineEmits<{
  select: [id: number];
  close: [];
}>();

const label = computed(() => (props.isActive ? "활성" : "비활성"));
</script>

<template>
  <div :class="{ active: isActive }" @click="emit('select', userId)">
    
  </div>
</template>
```

---

### Pinia 스토어

- 스토어는 **Setup Store** 방식(`defineStore` + `setup()` 형태)을 사용한다.
- 스토어 파일은 `src/stores/` 아래에 화면 구조와 동일한 경로로 분리한다. (예: `stores/settings/wifi.ts`)
- 스토어 ID는 파일 경로와 일치시킨다. (예: `'settings/wifi'`, `'auth'`)
- **스토어가 `network/api/`를 직접 호출하는 유일한 진입점이다.** View나 컴포넌트는 스토어만 바라본다.
- 스토어 내부에서 직접 `router.push()`를 호출하지 않는다. 라우팅은 View에서 처리한다.
- 로딩·에러 상태는 스토어 안에서 관리한다.

```ts
// src/stores/settings/audio.ts
import { ref } from "vue";
import { defineStore } from "pinia";
import {
  fetchAudioConfig,
  updateAudioConfig,
} from "@/network/api/settings/audio";
import type { AudioConfig } from "@/types";

export const useAudioStore = defineStore("settings/audio", () => {
  const config = ref<AudioConfig | null>(null);
  const isLoading = ref(false);
  const error = ref<string | null>(null);

  async function loadConfig() {
    isLoading.value = true;
    error.value = null;
    try {
      config.value = await fetchAudioConfig();
    } catch (e) {
      error.value = "오디오 설정을 불러오지 못했습니다.";
    } finally {
      isLoading.value = false;
    }
  }

  async function saveConfig(next: AudioConfig) {
    await updateAudioConfig(next);
    config.value = next;
  }

  return { config, isLoading, error, loadConfig, saveConfig };
});
```

---

### API 클라이언트 (`network/api/`)

**데이터 흐름**: `View``Store``network/api/` (단방향, 역방향 금지)

- Axios 인스턴스는 `src/network/api/client.ts` 한 곳에서만 생성한다.
- 인증 토큰 주입과 공통 에러 처리는 인터셉터에서 일괄 처리한다.
- `network/api/` 함수는 **순수 HTTP 호출만** 담당한다. 상태(`ref`, `reactive`)를 갖지 않는다.
- `network/api/` 함수는 반드시 스토어를 통해서만 호출한다. View나 컴포넌트에서 직접 호출하지 않는다.
- API 함수는 화면 구조와 동일한 경로로 분리한다. (예: `network/api/settings/wifi.ts`)
- 모든 API 함수의 반환 타입을 명시한다.

```ts
// src/network/api/client.ts
import axios from "axios";

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10_000,
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

apiClient.interceptors.response.use(
  (res) => res,
  (err) => {
    // 공통 에러 처리 (401 → 로그아웃 등)
    return Promise.reject(err);
  },
);
```

```ts
// src/network/api/settings/wifi.ts
import { apiClient } from "@/network/api/client";
import type { WifiNetwork, WifiConfig } from "@/types";

export async function fetchWifiNetworks(): Promise<WifiNetwork[]> {
  const { data } = await apiClient.get<WifiNetwork[]>(
    "/settings/wifi/networks",
  );
  return data;
}

export async function updateWifiConfig(config: WifiConfig): Promise<void> {
  await apiClient.put("/settings/wifi/config", config);
}
```

```ts
// src/stores/settings/wifi.ts  ← 스토어가 API를 wrapping
import { ref } from "vue";
import { defineStore } from "pinia";
import {
  fetchWifiNetworks,
  updateWifiConfig,
} from "@/network/api/settings/wifi";
import type { WifiNetwork, WifiConfig } from "@/types";

export const useWifiStore = defineStore("settings/wifi", () => {
  const networks = ref<WifiNetwork[]>([]);
  const isLoading = ref(false);
  const error = ref<string | null>(null);

  async function loadNetworks() {
    isLoading.value = true;
    error.value = null;
    try {
      networks.value = await fetchWifiNetworks();
    } catch (e) {
      error.value = "Wi-Fi 목록을 불러오지 못했습니다.";
    } finally {
      isLoading.value = false;
    }
  }

  async function saveConfig(config: WifiConfig) {
    await updateWifiConfig(config);
  }

  return { networks, isLoading, error, loadNetworks, saveConfig };
});
```

---

### Vue Router

- 라우트 설정은 `src/router/index.ts`에서 관리한다.
- 코드 스플리팅을 위해 페이지 컴포넌트는 `() => import(...)` 방식으로 지연 로딩한다.
- 라우트 이름은 `kebab-case` 문자열로 통일한다. (예: `'user-detail'`)
- 인증이 필요한 라우트는 `meta: { requiresAuth: true }`로 표시하고, 네비게이션 가드에서 일괄 처리한다.

```ts
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: () => import("@/views/HomeView.vue"),
    },
    {
      path: "/dashboard",
      name: "dashboard",
      component: () => import("@/views/dashboard/DashboardView.vue"),
      meta: { requiresAuth: true },
    },
    {
      path: "/settings/wifi",
      name: "settings-wifi",
      component: () => import("@/views/settings/WifiSettingView.vue"),
      meta: { requiresAuth: true },
    },
    {
      path: "/settings/audio",
      name: "settings-audio",
      component: () => import("@/views/settings/AudioSettingView.vue"),
      meta: { requiresAuth: true },
    },
  ],
});

router.beforeEach((to) => {
  const isLoggedIn = !!localStorage.getItem("token");
  if (to.meta.requiresAuth && !isLoggedIn) {
    return { name: "login" };
  }
});

export default router;
```

---

### 타입 정의

- 타입은 `src/types/index.ts`에 정의하고 프로젝트 전역에서 공유한다.
- API 응답 타입과 UI 상태 타입을 혼용하지 않는다. 필요하면 별도 인터페이스로 분리한다.
- `enum` 대신 `as const` 객체를 사용한다.

```ts
// src/types/index.ts
export interface WifiNetwork {
  ssid: string;
  signalStrength: number;
  isConnected: boolean;
}

export interface WifiConfig {
  ssid: string;
  password: string;
}

export interface AudioConfig {
  volume: number;
  muted: boolean;
}

// enum 대신 as const 사용
export const ConnectionStatus = {
  Connected: "connected",
  Disconnected: "disconnected",
  Connecting: "connecting",
} as const;

export type ConnectionStatus =
  (typeof ConnectionStatus)[keyof typeof ConnectionStatus];
```

---

### 환경변수

- 환경변수는 `VITE_` 접두사를 붙여야 클라이언트에서 접근 가능하다.
- `.env` 파일은 `.gitignore`에 포함하고, `.env.example`을 항상 최신으로 유지한다.
- 코드에서 `import.meta.env.VITE_*`로 접근하며, `vite-env.d.ts`에 타입을 선언한다.

```ts
// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_BASE_URL: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}
```

---

## 네이밍 규칙 요약

| 대상              | 규칙                       | 예시                             |
| ----------------- | -------------------------- | -------------------------------- |
| View 파일         | PascalCase (`View` 접미사) | `WifiSettingView.vue`            |
| 컴포넌트 파일     | PascalCase                 | `BaseButton.vue`                 |
| Pinia 스토어 함수 | camelCase (`use` 접두사)   | `useWifiStore`                   |
| 타입 / 인터페이스 | PascalCase                 | `WifiNetwork`, `AudioConfig`     |
| 상수 객체         | PascalCase                 | `UserRole`                       |
| 일반 변수 / 함수  | camelCase                  | `fetchWifiNetworks`, `isLoading` |
| 라우트 이름       | kebab-case                 | `'settings-wifi'`                |
| 환경변수          | `VITE_` + UPPER_SNAKE_CASE | `VITE_API_BASE_URL`              |

---

## 하지 말아야 할 것

- Options API를 사용하지 않는다. 반드시 `<script setup>`을 쓴다.
- `any` 타입을 사용하지 않는다.
- View나 컴포넌트에서 `network/api/`를 직접 import하여 호출하지 않는다. 반드시 스토어를 거친다.
- 컴포넌트에서 `axios`를 직접 import하여 호출하지 않는다.
- composable(`use*` 함수)을 새로 만들지 않는다. 로직은 스토어에 둔다.
- `ref`로 감싼 배열·객체를 통째로 교체할 때는 `.value`에 대입한다. (`store.networks = []` ❌ → `store.networks.value = []` ✅)
- Pinia 스토어 안에서 라우터(`router.push`)를 직접 호출하지 않는다.
- `enum`을 사용하지 않는다. `as const` 객체로 대체한다.
- `views/` 전용 컴포넌트를 `src/components/`(전역)에 두지 않는다. 특정 View에서만 쓰는 컴포넌트는 `views/<domain>/components/`에 둔다.

CLAUDE.md 예제 (Python with UV)

|

CLAUDE.md 예제 (Python with UV)

# CLAUDE.md

이 파일은 Claude가 이 프로젝트에서 코드를 생성하거나 수정할 때 참조하는 가이드입니다.

---

## 프로젝트 개요

- **프레임워크**: FastAPI
- **패키지 매니저**: uv
- **DB**: PostgreSQL + SQLAlchemy (async)
- **유효성 검사**: Pydantic v2
- **테스트**: pytest + httpx
- **컨테이너**: Docker

---

## 프로젝트 구조

```
.
├── app/
│   ├── main.py          # FastAPI 앱 생성 및 라우터 등록
│   ├── config.py        # 환경변수 설정 (pydantic-settings)
│   ├── database.py      # SQLAlchemy 엔진 및 세션
│   ├── models.py        # SQLAlchemy ORM 모델
│   ├── schemas.py       # Pydantic 스키마 (Request / Response)
│   ├── crud.py          # DB CRUD 함수
│   └── routers/
│       └── <domain>.py  # 도메인별 APIRouter
├── tests/
│   ├── conftest.py
│   └── test_<domain>.py
├── pyproject.toml
├── uv.lock
├── .env
├── .env.local
├── .env.test
└── docker
    └── Dockerfile
```

---

## 개발 환경 및 주요 명령어

```bash
# 의존성 설치
uv sync

# 개발 서버 실행
uv run fastapi dev app/main.py

# 프로덕션 서버 실행
uv run fastapi run app/main.py

# 패키지 추가
uv add <package>
uv add --dev <package>       # 개발 전용

# 테스트 실행
uv run pytest                # 전체
uv run pytest -v             # 상세 출력
uv run pytest tests/test_foo.py  # 특정 파일

# Docker
docker compose up --build    # 빌드 후 실행
docker compose up -d         # 백그라운드 실행
docker compose down          # 종료
```

---

## 코드 규칙

### FastAPI

- 라우터는 `app/routers/` 아래 도메인별 파일로 분리한다.
- 라우터는 `APIRouter(prefix="/...", tags=["..."])`로 생성하고 `main.py`에서 `app.include_router()`로 등록한다.
- 경로 함수는 `async def`를 사용한다.
- 응답 모델은 항상 `response_model=`로 명시한다.
- HTTP 예외는 `raise HTTPException(status_code=..., detail=...)`을 사용한다.

```python
# app/routers/items.py 예시
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app import crud, schemas
from app.database import get_db

router = APIRouter(prefix="/items", tags=["items"])

@router.get("/{item_id}", response_model=schemas.ItemResponse)
async def get_item(item_id: int, db: AsyncSession = Depends(get_db)):
    item = await crud.get_item(db, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")
    return item
```

### Pydantic v2

- 스키마는 `app/schemas.py`에 모아서 관리한다.
- Request / Response 스키마를 명확히 분리한다.
- `model_config = ConfigDict(from_attributes=True)`를 Response 스키마에 반드시 적용한다.
- `field_validator` 또는 `model_validator`를 활용하고, v1 스타일 (`@validator`)은 사용하지 않는다.

```python
from pydantic import BaseModel, ConfigDict, field_validator

class ItemCreate(BaseModel):
    name: str
    price: float

    @field_validator("price")
    @classmethod
    def price_must_be_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError("price must be positive")
        return v

class ItemResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    name: str
    price: float
```

### SQLAlchemy (Async)

- 엔진은 `create_async_engine`, 세션은 `AsyncSession`을 사용한다.
- 세션은 `async_sessionmaker`로 생성하고 `get_db` 의존성을 통해 주입한다.
- ORM 모델은 `app/models.py`에 정의한다.
- CRUD 함수는 `app/crud.py`에 모으고, 각 함수는 `db: AsyncSession`을 첫 번째 인자로 받는다.

```python
# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.config import settings

engine = create_async_engine(settings.database_url, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session
```

### 환경변수 (pydantic-settings)

- 환경변수는 `app/config.py``Settings` 클래스로 관리한다.
- `.env` 파일을 읽어 주입한다. `.env``.gitignore`에 포함하고, `.env.example`을 항상 최신으로 유지한다.
- 전역 인스턴스 `settings`를 통해 접근한다.

```python
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

    database_url: str
    secret_key: str = "changeme"
    debug: bool = False

settings = Settings()
```

---

## 테스트 규칙

- 테스트 파일은 `tests/test_<domain>.py` 형식으로 작성한다.
- `conftest.py`에서 테스트용 DB 세션과 `AsyncClient`를 픽스처로 제공한다.
- 테스트 DB는 인메모리 SQLite (`sqlite+aiosqlite:///:memory:`) 또는 별도 PostgreSQL 테스트 DB를 사용한다.
- 각 테스트는 독립적으로 실행 가능해야 하며, 픽스처로 상태를 초기화한다.

```python
# tests/conftest.py 예시
import pytest_asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.main import app
from app.database import get_db
from app.models import Base

TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"

@pytest_asyncio.fixture
async def client():
    engine = create_async_engine(TEST_DATABASE_URL)
    TestSession = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async def override_get_db():
        async with TestSession() as session:
            yield session

    app.dependency_overrides[get_db] = override_get_db

    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
        yield ac

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
```

---

## Docker 구성

- `Dockerfile``uv`를 사용해 의존성을 설치하고 `uv run`으로 앱을 실행한다.
- `docker-compose.yml`에서 PostgreSQL 서비스와 앱 서비스를 함께 정의한다.
- DB URL 등 민감한 값은 `.env`를 통해 주입한다.

```dockerfile
# Dockerfile 예시
FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

COPY app/ ./app/

CMD ["uv", "run", "fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"]
```

```yaml
# docker-compose.yml 예시
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"

  app:
    build: .
    env_file: .env
    ports:
      - "8000:8000"
    depends_on:
      - db
```

---

## 하지 말아야 할 것

- `requirements.txt`를 직접 편집하지 않는다. 의존성은 항상 `uv add` / `uv remove`로 관리한다.
- `uv.lock`을 수동으로 수정하지 않는다.
- SQLAlchemy 동기 세션(`Session`)을 사용하지 않는다. 반드시 `AsyncSession`을 쓴다.
- Pydantic v1 스타일 (`@validator`, `orm_mode = True`)을 사용하지 않는다.
- 환경변수를 코드에 하드코딩하지 않는다. 항상 `settings`를 통해 접근한다.
- 라우터 함수 안에 직접 SQL을 작성하지 않는다. CRUD 함수로 분리한다.

AI 활용 개발 방법론

|

개발 방법론

AI를 활용한 개발이지만 퀄리티를 확보하기 위해서는 설계가 필요
거창한 UML, 아키텍처 디자인, ERD, 기능 목록서, 인터페이스 정의서 등의 복잡한 문서까지는 아니지만
실용적 레벨의 설계는 필요

항목 설명
요구사항 분석 무엇을 만들 것인지, 기능 및 품질 속성 수립
아키텍처 결정 어떤 기술과 구조로 만들 것인지
인터페이스 정의 API 호출 방식이나 규격 정의

작은 단위로 쪼개기

처음부터 완벽한 설계는 불가능하기 때문에, 작은 단위부터 하나씩 진행하는 것이 좋음
AI를 활용해서 아래와 같은 순서로 진행할 수 있음

  1. 아이디어 정의
    “로봇 데이터 수집 현황 대시보드를 만들려고 해.
    JWT 로그인을 활용할 예정이고, 다양한 요소를 활용해 통계를 보여줄거야.
    어떤 요소들이 필요할까?”

  2. 설계 문서 초안 “위 요구사항으로 REQUIREMENTS.md” 초안 작성해줘

  3. 설계 검토 및 확장
    “이 설계 문서를 검토해서 개발 스펙에 추가해야 하거나 누락된 부분이 있는지 알려줘”

  4. 구현
    “REQUIREMENTS.md에 따라 백엔드 서비스 개발해줘”

점진적 설계

처음에는 핵심적인 아키텍처 결정만 먼저하고, 세부사항은 구현하면서 구체화할 수도 있다.

## 1차 설계 (프로젝트 시작 전)

- 배포 형태: On-Premise
- 기술 스택: FastAPI + PostgreSQL
- 인증 방식: JWT

## 2차 설계 (구현 중)

- API Endpoint 상세 정의
- Database Schema 확정
- 에러 코드 체계 정의

## 3차 설계 (통합 전)

- Frontend 컴포넌트 구조
- 상태 관리 전략
- API 응답 형식 표준화

한 번에 하나의 작업만 요청

AI는 동시에 여러 작업을 수행하는 능력이 떨어짐
여러 가지 일을 동시에 시키면 기능/코드 품질 저하, 복합적 에러로 인한 디버깅 어려움. 코드간 간섭 등의 어려운 문제가 발생할 수 있음

작업 분해의 단위

AI는 컨텍스트 윈도우 한계도 있기 때문에 작업이 커질수록 문제 발생 및 토큰 낭비 등이 발생할 수 있기 때문에
작업을 적절한 크기로 나누는 것이 좋다.
작업 분해는 다음 단위로 쪼개면 효율적이다.

  • SRP: 단일 책임 원칙
  • 테스트 가능한 단위
  • 10분 내 검증 가능한 단위
  • 파일 혹은 기능 단위

명확한 작업 지시

AI에게 어떤 것을 요청할 때 어떻게 동작하는지를 명확하게 요청하는 것이 더 좋음

ex)
“사용자 프로필 수정 기능을 구현해줘 “
보다는
“프로필 기능 수정. 비밀 번호 입력 후 본인 확인이 되면 프로필 수정 페이지가 나오고, username, password를 제외한 나머지 필드만 수정할 수 있도록 해야 해.”
가 더 좋은 지시사항

완료 조건 명시

## 완료 조건

다음 조건을 모두 만족하면 완료

1. 올바른 이메일/비밀번호로 로그인 시 JWT 토큰 반환
2. 잘못된 비밀번호로 5회 실패 시 계정 잠금
3. 잠긴 계정으로 로그인 시도 시 423 응답과 남은 시간 표시
4. 모든 케이스에 대한 단위 테스트 통과
5. TypeScript 컴파일 에러 없음