INIT(app): initial commit
- Initialize project structure - Add base configuration
This commit is contained in:
70
.dockerignore
Normal file
70
.dockerignore
Normal file
@@ -0,0 +1,70 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
|
||||
# Production
|
||||
dist/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
|
||||
# Scripts
|
||||
scripts/
|
||||
deploy/
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Trunk
|
||||
.trunk
|
||||
69
.github/workflows/build.yml
vendored
Normal file
69
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
image-tag: ${{ steps.meta.outputs.tags }}
|
||||
image-digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./services/nextjs
|
||||
file: ./deploy/docker/Dockerfile.prod
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Display image information
|
||||
run: |
|
||||
echo "✅ Image built and pushed successfully!"
|
||||
echo "📦 Image tags:"
|
||||
echo "${{ steps.meta.outputs.tags }}"
|
||||
echo "🔖 Digest: ${{ steps.build.outputs.digest }}"
|
||||
45
.github/workflows/ci.yml
vendored
Normal file
45
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: services/nextjs/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: services/nextjs
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
working-directory: services/nextjs
|
||||
run: npm run lint
|
||||
|
||||
- name: Build Next.js application
|
||||
working-directory: services/nextjs
|
||||
run: npm run build
|
||||
env:
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
- name: Check build output
|
||||
working-directory: services/nextjs
|
||||
run: |
|
||||
if [ ! -d ".next" ]; then
|
||||
echo "Build failed: .next directory not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build completed successfully"
|
||||
111
.github/workflows/deploy.yml
vendored
Normal file
111
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
name: Deploy to Kubernetes
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build Docker Image"]
|
||||
types:
|
||||
- completed
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy (e.g., main-abc1234)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
K8S_NAMESPACE: jovies
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Configure kubectl with Lightsail
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
|
||||
# Verify connection
|
||||
kubectl cluster-info
|
||||
kubectl get nodes
|
||||
|
||||
- name: Determine image tag
|
||||
id: image
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
TAG="${{ github.event.inputs.image_tag }}"
|
||||
else
|
||||
# Use the commit SHA from the workflow_run event
|
||||
TAG="main-$(echo ${{ github.sha }} | cut -c1-7)"
|
||||
fi
|
||||
|
||||
FULL_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "full_image=${FULL_IMAGE}" >> $GITHUB_OUTPUT
|
||||
echo "🐳 Deploying image: ${FULL_IMAGE}"
|
||||
|
||||
- name: Make scripts executable
|
||||
run: |
|
||||
chmod +x ./scripts/common.sh
|
||||
chmod +x ./scripts/k8s-deploy.sh
|
||||
|
||||
- name: Deploy to Kubernetes using script
|
||||
run: |
|
||||
./scripts/k8s-deploy.sh \
|
||||
--namespace ${{ env.K8S_NAMESPACE }} \
|
||||
--no-build \
|
||||
--app-image ${{ steps.image.outputs.full_image }}
|
||||
env:
|
||||
TERM: dumb
|
||||
|
||||
- name: Wait for rollout to complete
|
||||
run: |
|
||||
echo "⏳ Waiting for deployment rollout..."
|
||||
kubectl rollout status deployment/jovies-app \
|
||||
-n ${{ env.K8S_NAMESPACE }} \
|
||||
--timeout=5m
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "📊 Deployment status:"
|
||||
kubectl get deployments -n ${{ env.K8S_NAMESPACE }}
|
||||
|
||||
echo ""
|
||||
echo "🔍 Pod status:"
|
||||
kubectl get pods -n ${{ env.K8S_NAMESPACE }}
|
||||
|
||||
echo ""
|
||||
echo "🌐 Service status:"
|
||||
kubectl get services -n ${{ env.K8S_NAMESPACE }}
|
||||
|
||||
- name: Get deployment info
|
||||
run: |
|
||||
echo "✅ Deployment completed!"
|
||||
echo ""
|
||||
echo "📦 Deployed image: ${{ steps.image.outputs.full_image }}"
|
||||
echo "🏷️ Namespace: ${{ env.K8S_NAMESPACE }}"
|
||||
echo ""
|
||||
echo "🔗 Useful commands:"
|
||||
echo " - View logs: kubectl logs -n ${{ env.K8S_NAMESPACE }} -l app=jovies-app -f"
|
||||
echo " - Port forward: kubectl port-forward -n ${{ env.K8S_NAMESPACE }} deploy/jovies-app 3000:3000"
|
||||
echo " - Rollback: kubectl rollout undo deployment/jovies-app -n ${{ env.K8S_NAMESPACE }}"
|
||||
|
||||
- name: Deployment failure notification
|
||||
if: failure()
|
||||
run: |
|
||||
echo "❌ Deployment failed!"
|
||||
echo "Check logs with: kubectl logs -n ${{ env.K8S_NAMESPACE }} -l app=jovies-app"
|
||||
exit 1
|
||||
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
services/nextjs/node_modules
|
||||
services/nextjs/.pnp
|
||||
services/nextjs/.pnp.js
|
||||
services/nextjs/.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
services/nextjs/coverage
|
||||
|
||||
# next.js
|
||||
services/nextjs/.next/
|
||||
services/nextjs/out/
|
||||
|
||||
# production
|
||||
services/nextjs/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
|
||||
# trunk
|
||||
.trunk
|
||||
26
deploy/docker/Dockerfile.dev
Normal file
26
deploy/docker/Dockerfile.dev
Normal file
@@ -0,0 +1,26 @@
|
||||
# Development Dockerfile for Movie Next.js application
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies for development
|
||||
RUN apk add --no-cache libc6-compat curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install all dependencies (including dev dependencies)
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:3000 || exit 1
|
||||
|
||||
# Default command for development
|
||||
CMD ["npm", "run", "dev"]
|
||||
59
deploy/docker/Dockerfile.prod
Normal file
59
deploy/docker/Dockerfile.prod
Normal file
@@ -0,0 +1,59 @@
|
||||
# Multi-stage build for Movie Next.js application
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat curl
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:3000 || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
27
deploy/docker/docker-compose.dev.yml
Normal file
27
deploy/docker/docker-compose.dev.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
# Development Jovies Next.js Application
|
||||
app:
|
||||
build:
|
||||
context: ../../services
|
||||
dockerfile: ../deploy/docker/Dockerfile.dev
|
||||
container_name: jovies-app-dev
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
kompose.namespace: jovies
|
||||
ports:
|
||||
- 3003:3000
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- WATCHPACK_POLLING=true
|
||||
networks:
|
||||
- jovies-network
|
||||
volumes:
|
||||
- ../../services:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
command: npm run dev
|
||||
|
||||
networks:
|
||||
jovies-network:
|
||||
driver: bridge
|
||||
name: jovies-network-dev
|
||||
22
deploy/docker/docker-compose.yml
Normal file
22
deploy/docker/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
# Production Jovies Next.js Application
|
||||
app:
|
||||
image: jovies-app
|
||||
build:
|
||||
context: ../../services/nextjs
|
||||
dockerfile: ../../deploy/docker/Dockerfile.prod
|
||||
container_name: jovies-app-prod
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
kompose.namespace: jovies
|
||||
ports:
|
||||
- 3003:3000
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- jovies-network
|
||||
|
||||
networks:
|
||||
jovies-network:
|
||||
driver: bridge
|
||||
name: jovies-network-prod
|
||||
50
deploy/k8s/app-deployment.yaml
Normal file
50
deploy/k8s/app-deployment.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: jovies-app
|
||||
name: jovies-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jovies-app
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jovies-app
|
||||
spec:
|
||||
containers:
|
||||
- name: jovies-app
|
||||
image: jovies-app-image:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
restartPolicy: Always
|
||||
16
deploy/k8s/app-service.yaml
Normal file
16
deploy/k8s/app-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: jovies-app
|
||||
version: prod
|
||||
name: jovies-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: "http"
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: jovies-app
|
||||
180
scripts/common.sh
Executable file
180
scripts/common.sh
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jovies 스크립트 공통 유틸리티 함수들
|
||||
# 모든 Jovies 스크립트에서 사용할 수 있는 공통 기능들을 정의
|
||||
|
||||
set -e
|
||||
|
||||
# 공통 스크립트의 절대 경로 기반 디렉토리 상수
|
||||
# 함수 호출 컨텍스트에 따라 BASH_SOURCE 해석이 달라질 수 있으므로
|
||||
# 로드 시점에 고정해 신뢰 가능한 루트를 계산한다
|
||||
JOVIES_SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
JOVIES_ROOT="$(dirname "$JOVIES_SCRIPTS_DIR")"
|
||||
|
||||
# 색상 정의
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 로깅 함수들
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_debug() {
|
||||
echo -e "${BLUE}[DEBUG]${NC} $1"
|
||||
}
|
||||
|
||||
# 경로 계산 함수
|
||||
get_jovies_root() {
|
||||
# 로드 시점에 고정된 루트 경로 반환
|
||||
echo "$JOVIES_ROOT"
|
||||
}
|
||||
|
||||
get_mayne_root() {
|
||||
# jovies 루트를 기준으로 mayne 루트 경로 계산
|
||||
local jovies_root="$(get_jovies_root)"
|
||||
echo "$(dirname "$(dirname "$jovies_root")")"
|
||||
}
|
||||
|
||||
# 확인 함수
|
||||
confirm_action() {
|
||||
local message="$1"
|
||||
local default="${2:-N}"
|
||||
|
||||
if [[ "$default" == "Y" ]]; then
|
||||
read -p "$message (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
read -p "$message (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 필수 디렉토리 확인
|
||||
check_required_dirs() {
|
||||
local jovies_root="$(get_jovies_root)"
|
||||
local dirs=("$@")
|
||||
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$jovies_root/$dir" ]; then
|
||||
log_error "필수 디렉토리가 없습니다: $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 필수 파일 확인
|
||||
check_required_files() {
|
||||
local jovies_root="$(get_jovies_root)"
|
||||
local files=("$@")
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ ! -f "$jovies_root/$file" ]; then
|
||||
log_error "필수 파일이 없습니다: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Docker 관련 유틸리티
|
||||
docker_cleanup_jovies() {
|
||||
log_info "Jovies 관련 Docker 리소스 정리 중..."
|
||||
|
||||
# 컨테이너 중지 및 삭제
|
||||
docker-compose -p jovies -f deploy/docker/docker-compose.yml down --remove-orphans 2>/dev/null || true
|
||||
docker-compose -p jovies -f deploy/docker/docker-compose.dev.yml down --remove-orphans 2>/dev/null || true
|
||||
local containers=$(docker ps -aq --filter "name=jovies" 2>/dev/null)
|
||||
if [[ -n "$containers" ]]; then
|
||||
echo "$containers" | xargs docker rm -f 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 이미지 삭제
|
||||
local images=$(docker images --filter "reference=jovies*" -q 2>/dev/null)
|
||||
if [[ -n "$images" ]]; then
|
||||
echo "$images" | xargs docker rmi -f 2>/dev/null || true
|
||||
fi
|
||||
images=$(docker images --filter "reference=*jovies*" -q 2>/dev/null)
|
||||
if [[ -n "$images" ]]; then
|
||||
echo "$images" | xargs docker rmi -f 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 볼륨 삭제
|
||||
local volumes=$(docker volume ls -q --filter "name=jovies" 2>/dev/null)
|
||||
if [[ -n "$volumes" ]]; then
|
||||
echo "$volumes" | xargs docker volume rm 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 네트워크 삭제
|
||||
docker network rm jovies-network-dev jovies-network-prod 2>/dev/null || true
|
||||
|
||||
# 시스템 정리
|
||||
docker system prune -f
|
||||
|
||||
log_info "Docker 리소스 정리 완료"
|
||||
}
|
||||
|
||||
# 환경 변수 로드
|
||||
load_env_file() {
|
||||
local jovies_root="$(get_jovies_root)"
|
||||
local env_file="$jovies_root/.env"
|
||||
|
||||
if [ -f "$env_file" ]; then
|
||||
log_info "환경 변수 파일 로드: $env_file"
|
||||
source "$env_file"
|
||||
return 0
|
||||
else
|
||||
log_warn "환경 변수 파일이 없습니다: $env_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 에러 처리
|
||||
handle_error() {
|
||||
local exit_code=$?
|
||||
log_error "스크립트 실행 중 오류가 발생했습니다 (종료 코드: $exit_code)"
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# 스크립트 시작 시 공통 설정
|
||||
setup_script() {
|
||||
# 에러 발생 시 자동으로 handle_error 함수 호출
|
||||
trap handle_error ERR
|
||||
|
||||
# 스크립트 디렉토리로 이동
|
||||
cd "$(get_jovies_root)"
|
||||
|
||||
log_info "스크립트 시작: $(basename "${BASH_SOURCE[1]}")"
|
||||
}
|
||||
|
||||
# 스크립트 종료 시 정리
|
||||
cleanup_script() {
|
||||
local exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
log_info "스크립트 완료: $(basename "${BASH_SOURCE[1]}")"
|
||||
else
|
||||
log_error "스크립트 실패: $(basename "${BASH_SOURCE[1]}") (종료 코드: $exit_code)"
|
||||
fi
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# 스크립트 종료 시 정리 함수 등록
|
||||
trap cleanup_script EXIT
|
||||
96
scripts/docker-build.sh
Executable file
96
scripts/docker-build.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jovies Docker 빌드 및 실행 스크립트
|
||||
# 공통 유틸리티 함수 로드
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# 스크립트 설정
|
||||
setup_script
|
||||
|
||||
log_info "🚀 Jovies Docker 빌드 및 실행 시작..."
|
||||
|
||||
# 필수 디렉토리 및 파일 확인
|
||||
log_info "📁 폴더 구조 확인 중..."
|
||||
check_required_dirs "services" "deploy/docker"
|
||||
|
||||
log_info "📄 필수 파일 확인 중..."
|
||||
check_required_files "deploy/docker/docker-compose.yml" "deploy/docker/docker-compose.dev.yml" "deploy/docker/Dockerfile.prod" "deploy/docker/Dockerfile.dev"
|
||||
|
||||
log_info "✅ 폴더 구조 및 필수 파일 확인 완료!"
|
||||
|
||||
# 환경 선택
|
||||
echo ""
|
||||
log_info "🎯 실행할 환경을 선택하세요:"
|
||||
echo "1) 개발 환경 (Development)"
|
||||
echo "2) 프로덕션 환경 (Production)"
|
||||
echo "3) 빌드만 (Build Only)"
|
||||
read -p "선택 (1-3): " -n 1 -r
|
||||
echo
|
||||
|
||||
case ${REPLY} in
|
||||
1)
|
||||
log_info "🔧 개발 환경 빌드 및 실행 중..."
|
||||
cd deploy/docker
|
||||
docker-compose -p jovies -f docker-compose.dev.yml build --no-cache
|
||||
docker-compose -p jovies -f docker-compose.dev.yml up -d
|
||||
JOVIES_ROOT=$(get_jovies_root)
|
||||
cd "${JOVIES_ROOT}"
|
||||
ENV_TYPE="development"
|
||||
COMPOSE_FILE_PATH="deploy/docker/docker-compose.dev.yml"
|
||||
;;
|
||||
2)
|
||||
log_info "🏭 프로덕션 환경 빌드 및 실행 중..."
|
||||
cd deploy/docker
|
||||
docker-compose -p jovies -f docker-compose.yml build --no-cache
|
||||
docker-compose -p jovies -f docker-compose.yml up -d
|
||||
JOVIES_ROOT=$(get_jovies_root)
|
||||
cd "${JOVIES_ROOT}"
|
||||
ENV_TYPE="production"
|
||||
COMPOSE_FILE_PATH="deploy/docker/docker-compose.yml"
|
||||
;;
|
||||
3)
|
||||
log_info "🔨 이미지 빌드만 실행 중..."
|
||||
cd deploy/docker
|
||||
log_info " - 개발 이미지 빌드 중..."
|
||||
docker-compose -p jovies -f docker-compose.dev.yml build --no-cache
|
||||
log_info " - 프로덕션 이미지 빌드 중..."
|
||||
docker-compose -p jovies -f docker-compose.yml build --no-cache
|
||||
JOVIES_ROOT=$(get_jovies_root)
|
||||
cd "${JOVIES_ROOT}"
|
||||
log_info "✅ 빌드 완료! 실행하려면 다시 이 스크립트를 실행하고 환경을 선택하세요."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "잘못된 선택입니다."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 서비스 상태 확인
|
||||
echo ""
|
||||
log_info "⏳ 서비스 시작 대기 중..."
|
||||
sleep 10
|
||||
|
||||
echo ""
|
||||
log_info "📊 서비스 상태 확인:"
|
||||
docker-compose -p jovies -f "$COMPOSE_FILE_PATH" ps
|
||||
|
||||
echo ""
|
||||
log_info "🔍 컨테이너 로그 확인:"
|
||||
echo " - 애플리케이션 로그: docker-compose -p jovies -f $COMPOSE_FILE_PATH logs -f app"
|
||||
|
||||
echo ""
|
||||
log_info "🌐 접속 URL:"
|
||||
if [ "$ENV_TYPE" = "development" ]; then
|
||||
echo " - 애플리케이션: http://localhost:3002"
|
||||
else
|
||||
echo " - 애플리케이션: http://localhost:3002"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "✅ Docker 빌드 및 실행 완료!"
|
||||
echo ""
|
||||
log_info "📋 유용한 명령어:"
|
||||
echo " - 서비스 중지: docker-compose -p jovies -f $COMPOSE_FILE_PATH down"
|
||||
echo " - 로그 확인: docker-compose -p jovies -f $COMPOSE_FILE_PATH logs -f"
|
||||
echo " - 서비스 재시작: docker-compose -p jovies -f $COMPOSE_FILE_PATH restart"
|
||||
21
scripts/docker-cleanup.sh
Executable file
21
scripts/docker-cleanup.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jovies Docker Cleanup Script
|
||||
# 공통 유틸리티 함수 로드
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# 스크립트 설정
|
||||
setup_script
|
||||
|
||||
log_info "🧹 Jovies Docker 리소스 정리 시작..."
|
||||
|
||||
# 확인 메시지
|
||||
if ! confirm_action "모든 Jovies 관련 Docker 리소스를 정리하시겠습니까?"; then
|
||||
log_info "정리를 취소했습니다."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Docker 리소스 정리 실행
|
||||
docker_cleanup_jovies
|
||||
|
||||
log_info "✅ Jovies Docker 리소스 정리 완료!"
|
||||
112
scripts/k8s-cleanup.sh
Executable file
112
scripts/k8s-cleanup.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jovies Kubernetes 정리 스크립트
|
||||
# 공통 유틸리티 함수 로드
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# 스크립트 설정
|
||||
setup_script
|
||||
|
||||
print_usage() {
|
||||
echo ""
|
||||
echo "사용법: $(basename "$0") [-n NAMESPACE] [--context CONTEXT] [--delete-namespace] [--delete-images] [--force]"
|
||||
echo " -n, --namespace 정리할 네임스페이스 (기본: jovies)"
|
||||
echo " --context kubectl 컨텍스트 지정"
|
||||
echo " --delete-namespace 네임스페이스 자체를 삭제 (주의)"
|
||||
echo " --delete-images 로컬 Docker의 jovies 관련 이미지 삭제"
|
||||
echo " --force 확인 프롬프트 없이 진행"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 기본값
|
||||
K8S_NAMESPACE="jovies"
|
||||
KUBE_CONTEXT=""
|
||||
DELETE_NAMESPACE="true"
|
||||
DELETE_IMAGES="true"
|
||||
FORCE="false"
|
||||
|
||||
# 인자 파싱
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--namespace)
|
||||
K8S_NAMESPACE="$2"; shift; shift ;;
|
||||
--context)
|
||||
KUBE_CONTEXT="$2"; shift; shift ;;
|
||||
--delete-namespace)
|
||||
DELETE_NAMESPACE="true"; shift ;;
|
||||
--delete-images)
|
||||
DELETE_IMAGES="true"; shift ;;
|
||||
--force)
|
||||
FORCE="true"; shift ;;
|
||||
-h|--help)
|
||||
print_usage; exit 0 ;;
|
||||
*)
|
||||
log_warn "알 수 없는 인자: $1"; print_usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 컨텍스트 설정
|
||||
if [[ -n "$KUBE_CONTEXT" ]]; then
|
||||
log_info "kubectl 컨텍스트 설정: $KUBE_CONTEXT"
|
||||
kubectl config use-context "$KUBE_CONTEXT"
|
||||
else
|
||||
CURRENT_CTX=$(kubectl config current-context 2>/dev/null || echo "")
|
||||
if [[ -n "$CURRENT_CTX" ]]; then
|
||||
log_info "현재 kubectl 컨텍스트: $CURRENT_CTX"
|
||||
else
|
||||
log_warn "kubectl 컨텍스트가 설정되어 있지 않습니다. kubeconfig 를 확인하세요."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 네임스페이스 존재 확인
|
||||
if ! kubectl get namespace "$K8S_NAMESPACE" >/dev/null 2>&1; then
|
||||
log_warn "네임스페이스가 존재하지 않습니다: $K8S_NAMESPACE"
|
||||
if [[ "$DELETE_IMAGES" == "false" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "🧹 정리 대상 요약:"
|
||||
echo " - 네임스페이스: $K8S_NAMESPACE"
|
||||
if [[ "$DELETE_NAMESPACE" == "true" ]]; then echo " - 동작: 네임스페이스 삭제"; else echo " - 동작: 네임스페이스 내부 리소스 삭제"; fi
|
||||
if [[ "$DELETE_IMAGES" == "true" ]]; then echo " - 로컬 이미지 삭제: 활성화"; fi
|
||||
|
||||
# 확인 프롬프트
|
||||
if [[ "$FORCE" != "true" ]]; then
|
||||
if ! confirm_action "정리를 진행하시겠습니까?" "N"; then
|
||||
log_warn "사용자에 의해 취소되었습니다."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 리소스 삭제
|
||||
if [[ "$DELETE_NAMESPACE" == "true" ]]; then
|
||||
log_info "🔻 네임스페이스 삭제: $K8S_NAMESPACE"
|
||||
kubectl delete namespace "$K8S_NAMESPACE" --wait=false || true
|
||||
else
|
||||
log_info "🗑️ 네임스페이스 내부 리소스 삭제: $K8S_NAMESPACE"
|
||||
# 기본 리소스 (배포/서비스/ConfigMap/Secret/Job/CronJob 등) 일괄 삭제
|
||||
kubectl -n "$K8S_NAMESPACE" delete all --all || true
|
||||
# PVC 및 PV 삭제 (주의: 데이터 손실)
|
||||
kubectl -n "$K8S_NAMESPACE" delete pvc --all || true
|
||||
# 바운드 PV 중 네임스페이스 연관 PVC가 삭제된 경우 고아 PV 제거 시도
|
||||
for pv in $(kubectl get pv -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||||
claim_ns=$(kubectl get pv "$pv" -o jsonpath='{.spec.claimRef.namespace}' 2>/dev/null || echo "")
|
||||
if [[ "$claim_ns" == "$K8S_NAMESPACE" ]]; then
|
||||
log_info "📦 PV 삭제: $pv"
|
||||
kubectl delete pv "$pv" || true
|
||||
fi
|
||||
done
|
||||
# 남아있는 네임스페이스 스코프 리소스 정리
|
||||
kubectl -n "$K8S_NAMESPACE" delete configmap --all || true
|
||||
kubectl -n "$K8S_NAMESPACE" delete secret --all || true
|
||||
fi
|
||||
|
||||
# 로컬 Docker 이미지 삭제
|
||||
if [[ "$DELETE_IMAGES" == "true" ]]; then
|
||||
log_info "🧽 로컬 Docker 이미지 정리: jovies-app 관련 태그"
|
||||
docker rmi -f $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -E '^jovies-app:local|^jovies-app:' || true) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log_info "✅ 정리 작업 완료"
|
||||
163
scripts/k8s-deploy.sh
Executable file
163
scripts/k8s-deploy.sh
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jovies Kubernetes 배포 스크립트
|
||||
# 공통 유틸리티 함수 로드
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# 스크립트 설정
|
||||
setup_script
|
||||
|
||||
print_usage() {
|
||||
echo ""
|
||||
echo "사용법: $(basename "$0") [-n NAMESPACE] [-c CONTEXT] [--app-image IMAGE[:TAG]] [--build|--no-build] [--tag TAG] [--dry-run]"
|
||||
echo " -n, --namespace Kubernetes 네임스페이스 (기본: jovies)"
|
||||
echo " -c, --context kubectl 컨텍스트 지정 (kubectl config current-context 기본)"
|
||||
echo " --app-image app 디플로이먼트에 사용할 이미지 레퍼런스(예: your/repo:jovies)"
|
||||
echo " --build 배포 전에 Dockerfile로 로컬 이미지를 빌드 (기본)"
|
||||
echo " --no-build 빌드 생략"
|
||||
echo " --tag --build 시 사용할 이미지 태그 (기본: jovies-app:local-<timestamp>)"
|
||||
echo " --dry-run 실제 적용 대신 미리보기 수행"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 기본값
|
||||
K8S_NAMESPACE="jovies"
|
||||
KUBE_CONTEXT=""
|
||||
DRY_RUN="false"
|
||||
APP_IMAGE=""
|
||||
DO_BUILD="true"
|
||||
IMAGE_TAG=""
|
||||
|
||||
# 인자 파싱
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--namespace)
|
||||
K8S_NAMESPACE="$2"; shift; shift ;;
|
||||
-c|--context)
|
||||
KUBE_CONTEXT="$2"; shift; shift ;;
|
||||
--dry-run)
|
||||
DRY_RUN="true"; shift ;;
|
||||
--app-image)
|
||||
APP_IMAGE="$2"; shift; shift ;;
|
||||
--build)
|
||||
DO_BUILD="true"; shift ;;
|
||||
--no-build)
|
||||
DO_BUILD="false"; shift ;;
|
||||
--tag)
|
||||
IMAGE_TAG="$2"; shift; shift ;;
|
||||
-h|--help)
|
||||
print_usage; exit 0 ;;
|
||||
*)
|
||||
log_warn "알 수 없는 인자: $1"; print_usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 사전 점검
|
||||
check_required_dirs "deploy/k8s"
|
||||
|
||||
if ! command -v kubectl >/dev/null 2>&1; then
|
||||
log_error "kubectl 이 설치되어 있지 않습니다. 설치 후 다시 시도하세요."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v kompose >/dev/null 2>&1; then
|
||||
log_warn "kompose 가 설치되어 있지 않습니다. 기존 생성된 매니페스트만 적용합니다."
|
||||
fi
|
||||
|
||||
# K8S 디렉토리 설정 (prod 설정만 사용)
|
||||
K8S_DIR="deploy/k8s"
|
||||
|
||||
# 컨텍스트 설정
|
||||
if [[ -n "$KUBE_CONTEXT" ]]; then
|
||||
log_info "kubectl 컨텍스트 설정: $KUBE_CONTEXT"
|
||||
kubectl config use-context "$KUBE_CONTEXT"
|
||||
else
|
||||
CURRENT_CTX=$(kubectl config current-context 2>/dev/null || echo "")
|
||||
if [[ -n "$CURRENT_CTX" ]]; then
|
||||
log_info "현재 kubectl 컨텍스트: $CURRENT_CTX"
|
||||
else
|
||||
log_warn "kubectl 컨텍스트가 설정되어 있지 않습니다. kubeconfig 를 확인하세요."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 네임스페이스 보장
|
||||
if [[ "$K8S_NAMESPACE" != "default" ]]; then
|
||||
if ! kubectl get namespace "$K8S_NAMESPACE" >/dev/null 2>&1; then
|
||||
log_info "네임스페이스 생성: $K8S_NAMESPACE"
|
||||
kubectl create namespace "$K8S_NAMESPACE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 빌드가 필요한 경우 로컬 이미지 빌드
|
||||
if [[ "$DO_BUILD" == "true" && "$DRY_RUN" != "true" ]]; then
|
||||
BUILD_CONTEXT="."
|
||||
DOCKERFILE_PATH="deploy/docker/Dockerfile.prod"
|
||||
if [[ -z "$IMAGE_TAG" ]]; then
|
||||
IMAGE_TAG="jovies-app:local-$(date +%Y%m%d%H%M%S)"
|
||||
fi
|
||||
log_info "🔨 Docker 이미지 빌드: $IMAGE_TAG"
|
||||
docker build -t "$IMAGE_TAG" -f "$DOCKERFILE_PATH" "$BUILD_CONTEXT"
|
||||
# 빌드 결과를 배포 이미지로 기본 설정 (명시적으로 --app-image 주면 그 값이 우선)
|
||||
if [[ -z "$APP_IMAGE" ]]; then
|
||||
APP_IMAGE="$IMAGE_TAG"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "📦 적용 대상 디렉토리: $K8S_DIR"
|
||||
log_info "🧭 네임스페이스: $K8S_NAMESPACE"
|
||||
if [[ -n "$APP_IMAGE" ]]; then
|
||||
log_info "🖼️ 적용할 앱 이미지: $APP_IMAGE"
|
||||
fi
|
||||
|
||||
# Dry-run 플래그 구성
|
||||
APPLY_FLAGS=(apply -f "$K8S_DIR" -n "$K8S_NAMESPACE")
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
APPLY_FLAGS+=(--dry-run=client)
|
||||
fi
|
||||
|
||||
# 적용 전 요약 및 사전 검증
|
||||
log_info "📝 적용 요약:"
|
||||
echo " - 디렉토리: $K8S_DIR"
|
||||
echo " - 네임스페이스: $K8S_NAMESPACE"
|
||||
if [[ -n "$KUBE_CONTEXT" ]]; then echo " - 컨텍스트: $KUBE_CONTEXT"; fi
|
||||
if [[ "$DRY_RUN" == "true" ]]; then echo " - 모드: Dry Run"; fi
|
||||
if grep -qE "^\s*image:\s*app(\s|$)" "$K8S_DIR/app-deployment.yaml" 2>/dev/null; then
|
||||
if [[ -z "$APP_IMAGE" ]]; then
|
||||
log_warn "app-deployment.yaml 에 image: app 이 설정되어 있습니다. 실제 레지스트리 이미지를 --app-image 로 지정하는 것을 권장합니다."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 확인 후 진행
|
||||
if ! confirm_action "계속 진행하시겠습니까?" "Y"; then
|
||||
log_warn "사용자에 의해 취소되었습니다."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "🚀 Kubernetes 리소스 적용 중..."
|
||||
kubectl "${APPLY_FLAGS[@]}"
|
||||
|
||||
# 필요 시 이미지 오버라이드
|
||||
if [[ -n "$APP_IMAGE" ]]; then
|
||||
log_info "🔄 앱 디플로이먼트 이미지 업데이트: $APP_IMAGE"
|
||||
kubectl -n "$K8S_NAMESPACE" set image deployment/jovies-app jovies-app="$APP_IMAGE"
|
||||
# latest 태그나 기본 정책으로 인한 강제 Pull 방지
|
||||
log_info "🛠️ 이미지 Pull 정책 패치: IfNotPresent"
|
||||
kubectl -n "$K8S_NAMESPACE" patch deployment jovies-app \
|
||||
--type='json' \
|
||||
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"IfNotPresent"}]' || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "📊 리소스 상태:"
|
||||
kubectl get all -n "$K8S_NAMESPACE"
|
||||
|
||||
echo ""
|
||||
log_info "📄 유용한 명령어:"
|
||||
echo " - 삭제: kubectl delete -f $K8S_DIR -n $K8S_NAMESPACE"
|
||||
echo " - 로그: kubectl logs deploy/jovies-app -n $K8S_NAMESPACE -f"
|
||||
echo " - 상세: kubectl describe deploy/jovies-app -n $K8S_NAMESPACE"
|
||||
echo " - 포트 포워드: kubectl port-forward -n $K8S_NAMESPACE deploy/jovies-app 3002:3000"
|
||||
|
||||
log_info "✅ Kubernetes 배포 작업 완료"
|
||||
3
services/nextjs/.eslintrc.json
Normal file
3
services/nextjs/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
services/nextjs/README.md
Normal file
36
services/nextjs/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
5
services/nextjs/app/(movies)/movies/[id]/error.tsx
Normal file
5
services/nextjs/app/(movies)/movies/[id]/error.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
"use client"
|
||||
|
||||
export default function Error(){
|
||||
return <h1>Something broke...</h1>
|
||||
}
|
||||
3
services/nextjs/app/(movies)/movies/[id]/loading.tsx
Normal file
3
services/nextjs/app/(movies)/movies/[id]/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <h2>Loading a movie :id</h2>;
|
||||
}
|
||||
26
services/nextjs/app/(movies)/movies/[id]/page.tsx
Normal file
26
services/nextjs/app/(movies)/movies/[id]/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Suspense } from "react";
|
||||
import MovieInfo from "@/components/movie-info";
|
||||
import VideosInfo from "@/components/movie-videos";
|
||||
import { getMovie } from "@/components/movie-info";
|
||||
|
||||
interface Iparams {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: { id } }: Iparams) {
|
||||
const movie = await getMovie(id);
|
||||
return { title: movie.title };
|
||||
}
|
||||
|
||||
export default async function MovieDetail({ params: { id } }: Iparams) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<h1>Loading movie info</h1>}>
|
||||
<MovieInfo id={id} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<h1>Loading videos info</h1>}>
|
||||
<VideosInfo id={id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
services/nextjs/app/about-us/page.tsx
Normal file
3
services/nextjs/app/about-us/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function AboutUs(){
|
||||
return <div>gfdgd</div>
|
||||
}
|
||||
BIN
services/nextjs/app/favicon.ico
Normal file
BIN
services/nextjs/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
18
services/nextjs/app/layout.tsx
Normal file
18
services/nextjs/app/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import "../styles/global.css";
|
||||
import React from "react";
|
||||
import Navigation from "@/components/navigation";
|
||||
|
||||
export const metadata = {
|
||||
title: "movieClone",
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Navigation></Navigation>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
30
services/nextjs/app/page.tsx
Normal file
30
services/nextjs/app/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import styles from "../styles/home.module.css";
|
||||
import Movie from "@/components/movie";
|
||||
|
||||
const URL = "https://nomad-movies.nomadcoders.workers.dev/movies";
|
||||
|
||||
export const metadata = {
|
||||
title: "Jovies",
|
||||
};
|
||||
|
||||
async function getMovies() {
|
||||
const response = await fetch(URL);
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const movies = await getMovies();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{movies.map((movie: any) => (
|
||||
<Movie
|
||||
key={movie.id}
|
||||
id={movie.id}
|
||||
poster_path={movie.poster_path}
|
||||
title={movie.title}
|
||||
></Movie>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
services/nextjs/components/movie-info.tsx
Normal file
29
services/nextjs/components/movie-info.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import styles from "../styles/movie-info.module.css";
|
||||
|
||||
const URL = "https://nomad-movies.nomadcoders.workers.dev/movies";
|
||||
|
||||
export async function getMovie(id: string) {
|
||||
const response = await fetch(`${URL}/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export default async function MovieInfo({ id }: { id: string }) {
|
||||
const movie = await getMovie(id);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<img
|
||||
src={movie.poster_path}
|
||||
className={styles.poster}
|
||||
alt={movie.title}
|
||||
/>
|
||||
<div className={styles.info}>
|
||||
<h1 className={styles.title}>{movie.title}</h1>
|
||||
<h3>*{movie.vote_average.toFixed(1)}</h3>
|
||||
<p>{movie.overview}</p>
|
||||
<a href={movie.homepage} target={"_blank"}>
|
||||
Homepage →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
services/nextjs/components/movie-videos.tsx
Normal file
25
services/nextjs/components/movie-videos.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import styles from "../styles/movie-videos.module.css";
|
||||
|
||||
const URL = "https://nomad-movies.nomadcoders.workers.dev/movies";
|
||||
|
||||
async function getVideos(id: string) {
|
||||
const response = await fetch(`${URL}/${id}/videos`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export default async function VideosInfo({ id }: { id: string }) {
|
||||
const videos = await getVideos(id);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{videos.map((video: any) => (
|
||||
<iframe
|
||||
key={video.id}
|
||||
src={`https://youtube.com/embed/${video.key}`}
|
||||
title={video.name}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
services/nextjs/components/movie.tsx
Normal file
24
services/nextjs/components/movie.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import styles from "../styles/movie.module.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface IMovieProps {
|
||||
title: string;
|
||||
id: string;
|
||||
poster_path: string;
|
||||
}
|
||||
|
||||
export default function Movie({ title, id, poster_path }: IMovieProps) {
|
||||
const router = useRouter();
|
||||
const onClick = () => {
|
||||
router.push(`/movies/${id}`);
|
||||
};
|
||||
return (
|
||||
<div className={styles.movie}>
|
||||
<img src={poster_path} alt={title} onClick={onClick} />
|
||||
<Link href={`/movies/${id}`}>{title}</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
services/nextjs/components/navigation.tsx
Normal file
23
services/nextjs/components/navigation.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import styles from "../styles/navigation.module.css"
|
||||
|
||||
export default function Navigation() {
|
||||
const path = usePathname();
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/">Home</Link>
|
||||
{path === "/" ? "🔥" : ""}
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about-us">About Us</Link>
|
||||
{path === "/about-us" ? "🔥" : ""}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
6
services/nextjs/next.config.mjs
Normal file
6
services/nextjs/next.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
5478
services/nextjs/package-lock.json
generated
Normal file
5478
services/nextjs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
services/nextjs/package.json
Normal file
26
services/nextjs/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "nextjstutorial",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3"
|
||||
}
|
||||
}
|
||||
8
services/nextjs/postcss.config.mjs
Normal file
8
services/nextjs/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
154
services/nextjs/styles/global.css
Normal file
154
services/nextjs/styles/global.css
Normal file
@@ -0,0 +1,154 @@
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 150px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
"Open Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
8
services/nextjs/styles/home.module.css
Normal file
8
services/nextjs/styles/home.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 25px;
|
||||
max-width: 90%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
27
services/nextjs/styles/movie-info.module.css
Normal file
27
services/nextjs/styles/movie-info.module.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 50px;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.poster {
|
||||
border-radius: 20px;
|
||||
max-width: 70%;
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
19
services/nextjs/styles/movie-videos.module.css
Normal file
19
services/nextjs/styles/movie-videos.module.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 100px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.container iframe {
|
||||
border-radius: 10px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.container iframe:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
26
services/nextjs/styles/movie.module.css
Normal file
26
services/nextjs/styles/movie.module.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.movie {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 20px;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.movie img {
|
||||
max-width: 100%;
|
||||
min-height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.movie img {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.movie img:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.movie a {
|
||||
text-align: center;
|
||||
}
|
||||
28
services/nextjs/styles/navigation.module.css
Normal file
28
services/nextjs/styles/navigation.module.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.nav{
|
||||
background-color: #2d2d2d;
|
||||
position: fixed;
|
||||
width: 30%;
|
||||
margin: 0 auto;
|
||||
top: 20px;
|
||||
border-radius: 50px;
|
||||
padding: 20px 0px;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.nav ul li {
|
||||
list-style: none;
|
||||
transform: none;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.nav ul li:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
20
services/nextjs/tailwind.config.ts
Normal file
20
services/nextjs/tailwind.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
26
services/nextjs/tsconfig.json
Normal file
26
services/nextjs/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user