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