feat: claude nuxt 프로젝트 생성

This commit is contained in:
hyeonggil
2026-02-22 22:04:22 +09:00
commit 90447622e1
28 changed files with 9931 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"WebFetch(domain:nuxt.com)",
"WebFetch(domain:ui.shadcn.com)",
"WebFetch(domain:www.shadcn-vue.com)",
"Bash(pkill -f \"nuxt dev\" 2>/dev/null; echo \"완료\")"
]
}
}

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# Nuxt 개발 디렉토리
.nuxt
.output
# Node
node_modules
.node_modules
# 빌드 결과물
dist
# 환경 변수 (민감한 정보 보호)
.env
.env.*
!.env.example
# 로그
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 에디터
.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS
.DS_Store
Thumbs.db
# TypeScript 생성 파일
*.tsbuildinfo
# 테스트 커버리지
coverage
.nyc_output

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100,
"vueIndentScriptAndStyle": false
}

5
app/app.config.ts Normal file
View File

@@ -0,0 +1,5 @@
export default defineAppConfig({
// 앱 기본 정보
name: 'Nuxt 4 Starter',
description: 'Nuxt 4, TypeScript, TailwindCSS v4, shadcn-vue 기반 스타터 킷',
})

5
app/app.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

127
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,127 @@
@import "tailwindcss";
@import "tw-animate-css";
/* 다크모드 변형 — .dark 클래스 기반 (@nuxtjs/color-mode 호환) */
@custom-variant dark (&:is(.dark *));
/* TailwindCSS v4 테마 — shadcn-vue CSS 변수 연동 */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* 라이트 모드 — OKLCH 색상 값 (shadcn-vue init 생성) */
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
/* 다크 모드 */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
/* 기본 전역 스타일 */
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { type VariantProps, cva } from 'class-variance-authority'
import { cn } from '~/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
)
interface Props {
variant?: VariantProps<typeof badgeVariants>['variant']
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { type VariantProps, cva } from 'class-variance-authority'
import { cn } from '~/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
interface Props {
variant?: VariantProps<typeof buttonVariants>['variant']
size?: VariantProps<typeof buttonVariants>['size']
class?: string
as?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<component
:is="as"
:class="cn(buttonVariants({ variant, size }), props.class)"
:disabled="disabled"
>
<slot />
</component>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div :class="cn('rounded-xl border bg-card text-card-foreground shadow', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div :class="cn('flex flex-col space-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
<slot />
</h3>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes["class"]
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input
v-model="modelValue"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)"
>
</template>

View File

@@ -0,0 +1 @@
export { default as Input } from "./Input.vue"

46
app/error.vue Normal file
View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
interface Props {
error: {
statusCode: number
statusMessage: string
message: string
}
}
const props = defineProps<Props>()
const errorMessages: Record<number, string> = {
404: '페이지를 찾을 수 없습니다',
403: '접근 권한이 없습니다',
500: '서버 오류가 발생했습니다',
}
const errorTitle = computed(
() => errorMessages[props.error.statusCode] ?? '오류가 발생했습니다'
)
function handleError() {
clearError({ redirect: '/' })
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-background">
<div class="text-center px-4">
<p class="text-8xl font-bold text-muted-foreground/30 mb-4">
{{ error.statusCode }}
</p>
<h1 class="text-2xl font-semibold mb-2">{{ errorTitle }}</h1>
<p class="text-muted-foreground mb-8 max-w-md mx-auto">
{{ error.message || error.statusMessage }}
</p>
<button
@click="handleError"
class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
<Icon name="lucide:arrow-left" class="w-4 h-4" />
홈으로 돌아가기
</button>
</div>
</div>
</template>

68
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
// @nuxtjs/color-mode 제공 컴포저블
const colorMode = useColorMode()
const isDark = computed(() => colorMode.value === 'dark')
function toggleColorMode() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<div class="min-h-screen flex flex-col">
<!-- sticky 헤더 -->
<header
class="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div class="container mx-auto px-4 h-16 flex items-center justify-between">
<!-- 로고 -->
<NuxtLink
to="/"
class="font-bold text-xl tracking-tight hover:opacity-80 transition-opacity"
>
Nuxt 4 Starter
</NuxtLink>
<!-- 네비게이션 -->
<nav class="hidden md:flex items-center gap-6">
<NuxtLink
to="/"
class="text-sm font-medium hover:text-primary transition-colors"
active-class="text-primary"
>
</NuxtLink>
<NuxtLink
to="/about"
class="text-sm font-medium text-muted-foreground hover:text-primary transition-colors"
active-class="text-primary"
>
소개
</NuxtLink>
</nav>
<!-- 다크모드 토글 -->
<button
@click="toggleColorMode"
class="p-2 rounded-md hover:bg-accent transition-colors cursor-pointer"
:aria-label="isDark ? '라이트 모드로 전환' : '다크 모드로 전환'"
>
<Icon :name="isDark ? 'lucide:sun' : 'lucide:moon'" class="w-5 h-5" />
</button>
</div>
</header>
<!-- 메인 콘텐츠 -->
<main class="flex-1">
<slot />
</main>
<!-- 푸터 -->
<footer class="border-t py-8">
<div class="container mx-auto px-4 text-center text-sm text-muted-foreground">
<p>Nuxt 4 Starter Kit Built with Nuxt 4, TailwindCSS v4, shadcn-vue</p>
</div>
</footer>
</div>
</template>

7
app/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

23
app/pages/about.vue Normal file
View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
useSeoMeta({
title: '소개 — Nuxt 4 Starter Kit',
description: 'Nuxt 4 Starter Kit 소개 페이지',
})
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-2xl">
<h1 class="text-3xl font-bold mb-4">소개</h1>
<p class="text-muted-foreground mb-8"> 페이지를 자유롭게 수정하세요.</p>
<Card class="p-6">
<CardHeader class="p-0 mb-4">
<CardTitle>Nuxt 4 Starter Kit</CardTitle>
</CardHeader>
<CardContent class="p-0 space-y-2 text-sm text-muted-foreground">
<p>최신 기술 스택으로 빠르게 개발을 시작할 있는 스타터 킷입니다.</p>
<p>Nuxt 4, TypeScript, TailwindCSS v4, shadcn-vue를 기반으로 구성되어 있습니다.</p>
</CardContent>
</Card>
</div>
</template>

207
app/pages/index.vue Normal file
View File

@@ -0,0 +1,207 @@
<script setup lang="ts">
import { useCounterStore } from '~/stores/useCounterStore'
// SEO 메타 설정
useSeoMeta({
title: 'Nuxt 4 Starter Kit',
description: 'Nuxt 4, TypeScript, TailwindCSS v4, shadcn-vue 기반 스타터 킷',
})
const counter = useCounterStore()
// 기술 스택 목록
const techStack = [
{
icon: 'lucide:layers',
name: 'Nuxt 4',
description: 'Vue 기반 풀스택 프레임워크. app/ 디렉토리, 파일 기반 라우팅, SSR/SSG 지원.',
color: 'text-green-500',
bg: 'bg-green-500/10',
},
{
icon: 'lucide:code-2',
name: 'TypeScript',
description: 'Strict 모드 타입 안전성. any 타입 금지, 컨텍스트별 별도 tsconfig.',
color: 'text-blue-500',
bg: 'bg-blue-500/10',
},
{
icon: 'lucide:palette',
name: 'TailwindCSS v4',
description: 'CSS-first 설정. config 파일 없이 @theme으로 커스터마이징.',
color: 'text-cyan-500',
bg: 'bg-cyan-500/10',
},
{
icon: 'lucide:component',
name: 'shadcn-vue',
description: '복사해서 소유하는 UI 컴포넌트. Radix Vue 기반, 완전한 커스터마이징 가능.',
color: 'text-purple-500',
bg: 'bg-purple-500/10',
},
{
icon: 'lucide:database',
name: 'Pinia',
description: 'Vue 공식 상태 관리. Composition API 스타일, TypeScript 완벽 지원.',
color: 'text-yellow-500',
bg: 'bg-yellow-500/10',
},
{
icon: 'lucide:moon',
name: '다크 모드',
description: '@nuxtjs/color-mode로 다크/라이트 모드 전환. shadcn CSS 변수 연동.',
color: 'text-slate-500',
bg: 'bg-slate-500/10',
},
] as const
// 버튼 variant 목록
const buttonVariants = ['default', 'secondary', 'outline', 'ghost', 'destructive', 'link'] as const
</script>
<template>
<div class="container mx-auto px-4">
<!-- 히어로 섹션 -->
<section class="text-center py-20 mb-8">
<div class="flex flex-wrap items-center justify-center gap-2 mb-6">
<Badge>Nuxt 4</Badge>
<Badge variant="secondary">TypeScript</Badge>
<Badge variant="secondary">TailwindCSS v4</Badge>
<Badge variant="outline">shadcn-vue</Badge>
</div>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight mb-6">
Nuxt 4 Starter Kit
</h1>
<p class="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto mb-10">
최신 기술 스택으로 빠르게 개발을 시작하세요.
<br class="hidden sm:block" />
Nuxt 4 + TypeScript + TailwindCSS v4 + shadcn-vue
</p>
<div class="flex flex-wrap gap-3 justify-center">
<Button size="lg">
<Icon name="lucide:rocket" class="w-4 h-4 mr-2" />
시작하기
</Button>
<Button variant="outline" size="lg">
<Icon name="lucide:book-open" class="w-4 h-4 mr-2" />
문서 보기
</Button>
</div>
</section>
<!-- 기술 스택 섹션 -->
<section class="mb-16">
<h2 class="text-2xl font-semibold text-center mb-2">기술 스택</h2>
<p class="text-muted-foreground text-center mb-8">프로덕션 레디 기술 스택으로 구성</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card
v-for="tech in techStack"
:key="tech.name"
class="p-6 hover:shadow-md transition-shadow"
>
<CardHeader class="p-0 mb-4 flex-row items-center gap-3">
<div :class="['p-2 rounded-lg', tech.bg]">
<Icon :name="tech.icon" :class="['w-5 h-5', tech.color]" />
</div>
<CardTitle class="text-base">{{ tech.name }}</CardTitle>
</CardHeader>
<CardContent class="p-0">
<p class="text-sm text-muted-foreground">{{ tech.description }}</p>
</CardContent>
</Card>
</div>
</section>
<!-- 컴포넌트 쇼케이스 -->
<section class="mb-16">
<h2 class="text-2xl font-semibold text-center mb-2">컴포넌트 쇼케이스</h2>
<p class="text-muted-foreground text-center mb-8">shadcn-vue 기본 컴포넌트 예시</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 버튼 variants -->
<Card class="p-6">
<CardHeader class="p-0 mb-4">
<CardTitle>Button Variants</CardTitle>
</CardHeader>
<CardContent class="p-0">
<div class="flex flex-wrap gap-2">
<Button
v-for="variant in buttonVariants"
:key="variant"
:variant="variant"
size="sm"
>
{{ variant }}
</Button>
</div>
</CardContent>
</Card>
<!-- Badge variants -->
<Card class="p-6">
<CardHeader class="p-0 mb-4">
<CardTitle>Badge Variants</CardTitle>
</CardHeader>
<CardContent class="p-0">
<div class="flex flex-wrap gap-2">
<Badge>default</Badge>
<Badge variant="secondary">secondary</Badge>
<Badge variant="outline">outline</Badge>
<Badge variant="destructive">destructive</Badge>
</div>
</CardContent>
</Card>
</div>
</section>
<!-- Pinia 카운터 데모 -->
<section class="mb-16">
<h2 class="text-2xl font-semibold text-center mb-2">Pinia 상태 관리</h2>
<p class="text-muted-foreground text-center mb-8">useCounterStore 예시</p>
<div class="flex justify-center">
<Card class="p-8 text-center w-full max-w-sm">
<CardHeader class="p-0 mb-6">
<CardTitle>카운터</CardTitle>
</CardHeader>
<CardContent class="p-0 mb-6">
<p class="text-6xl font-bold tabular-nums">{{ counter.count }}</p>
</CardContent>
<CardFooter class="p-0 flex justify-center gap-2">
<Button
variant="outline"
size="icon"
@click="counter.decrement"
:disabled="counter.count <= 0"
>
<Icon name="lucide:minus" class="w-4 h-4" />
</Button>
<Button variant="outline" @click="counter.reset">초기화</Button>
<Button size="icon" @click="counter.increment">
<Icon name="lucide:plus" class="w-4 h-4" />
</Button>
</CardFooter>
</Card>
</div>
</section>
<!-- 다음 단계 안내 -->
<section class="mb-16">
<Card class="p-8 bg-muted/50">
<CardHeader class="p-0 mb-4">
<CardTitle class="text-xl text-center"> 컴포넌트 추가하기</CardTitle>
</CardHeader>
<CardContent class="p-0 text-center">
<p class="text-muted-foreground mb-4">shadcn-vue CLI로 컴포넌트를 추가하세요</p>
<code class="bg-background border rounded-md px-4 py-2 text-sm font-mono">
npx shadcn-vue@latest add [component]
</code>
</CardContent>
</Card>
</section>
</div>
</template>

View File

@@ -0,0 +1,22 @@
import { defineStore } from 'pinia'
// 카운터 스토어 — Pinia Composition API 스타일 예시
export const useCounterStore = defineStore('counter', () => {
// 상태
const count = ref<number>(0)
// 액션
function increment() {
count.value++
}
function decrement() {
if (count.value > 0) count.value--
}
function reset() {
count.value = 0
}
return { count, increment, decrement, reset }
})

7
app/utils/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
// shadcn-vue 표준 유틸: Tailwind 클래스 병합 (충돌 자동 해결)
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "",
"css": "app/assets/css/main.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"registries": {}
}

9
eslint.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
// 단일 단어 컴포넌트명 허용 (예: error.vue, index.vue)
'vue/multi-word-component-names': 'off',
},
})

39
nuxt.config.ts Normal file
View File

@@ -0,0 +1,39 @@
import tailwindcss from '@tailwindcss/vite'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
modules: [
'@nuxt/icon',
'@nuxtjs/color-mode',
'@pinia/nuxt',
],
// shadcn-vue UI 컴포넌트: pathPrefix: false → 'Button'으로 사용 (UiButton 아님)
// extensions: ['vue'] → index.ts 중복 등록 방지
components: [
{ path: '~/components/ui', pathPrefix: false, extensions: ['vue'] },
'~/components',
],
// 다크모드: suffix 없이 class 기반 (shadcn-vue 호환)
colorMode: {
classSuffix: '',
},
// 글로벌 CSS (공식 TailwindCSS 가이드 경로)
css: ['./app/assets/css/main.css'],
// TailwindCSS v4 Vite 플러그인 (공식 가이드)
vite: {
plugins: [tailwindcss()],
},
// TypeScript strict 모드
typescript: {
strict: true,
},
})

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "nuxt4-starter",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@nuxt/icon": "^1.12.0",
"@nuxtjs/color-mode": "^3.5.2",
"@pinia/nuxt": "^0.9.0",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.575.0",
"nuxt": "^4.0.0",
"pinia": "^2.3.0",
"radix-vue": "^1.9.9",
"tailwind-merge": "^2.6.0",
"vue": "^3.5.0",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.93",
"@nuxt/eslint": "^1.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"prettier": "^3.4.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.7.0"
}
}

9034
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}