feat: claude nuxt 프로젝트 생성
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal 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
42
.gitignore
vendored
Normal 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
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
5
app/app.config.ts
Normal file
5
app/app.config.ts
Normal 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
5
app/app.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
127
app/assets/css/main.css
Normal file
127
app/assets/css/main.css
Normal 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;
|
||||
}
|
||||
}
|
||||
37
app/components/ui/Badge.vue
Normal file
37
app/components/ui/Badge.vue
Normal 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>
|
||||
54
app/components/ui/Button.vue
Normal file
54
app/components/ui/Button.vue
Normal 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>
|
||||
15
app/components/ui/Card.vue
Normal file
15
app/components/ui/Card.vue
Normal 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>
|
||||
15
app/components/ui/CardContent.vue
Normal file
15
app/components/ui/CardContent.vue
Normal 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>
|
||||
15
app/components/ui/CardFooter.vue
Normal file
15
app/components/ui/CardFooter.vue
Normal 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>
|
||||
15
app/components/ui/CardHeader.vue
Normal file
15
app/components/ui/CardHeader.vue
Normal 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>
|
||||
15
app/components/ui/CardTitle.vue
Normal file
15
app/components/ui/CardTitle.vue
Normal 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>
|
||||
33
app/components/ui/input/Input.vue
Normal file
33
app/components/ui/input/Input.vue
Normal 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>
|
||||
1
app/components/ui/input/index.ts
Normal file
1
app/components/ui/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from "./Input.vue"
|
||||
46
app/error.vue
Normal file
46
app/error.vue
Normal 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
68
app/layouts/default.vue
Normal 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
7
app/lib/utils.ts
Normal 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
23
app/pages/about.vue
Normal 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
207
app/pages/index.vue
Normal 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>
|
||||
22
app/stores/useCounterStore.ts
Normal file
22
app/stores/useCounterStore.ts
Normal 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
7
app/utils/index.ts
Normal 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
21
components.json
Normal 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
9
eslint.config.mjs
Normal 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
39
nuxt.config.ts
Normal 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
43
package.json
Normal 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
9034
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user