commit 3c10907a973a579dc2136503ff0653b18157984e Author: Mayne0213 Date: Sat Nov 22 23:44:51 2025 +0900 INIT(app): initial commit - Initialize project structure - Add base configuration diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..35589ea --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1132309 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2879a5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e966597 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ddb3f6 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/deploy/docker/Dockerfile.dev b/deploy/docker/Dockerfile.dev new file mode 100644 index 0000000..b1dd32d --- /dev/null +++ b/deploy/docker/Dockerfile.dev @@ -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"] diff --git a/deploy/docker/Dockerfile.prod b/deploy/docker/Dockerfile.prod new file mode 100644 index 0000000..69db9e4 --- /dev/null +++ b/deploy/docker/Dockerfile.prod @@ -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"] diff --git a/deploy/docker/docker-compose.dev.yml b/deploy/docker/docker-compose.dev.yml new file mode 100644 index 0000000..fdebf42 --- /dev/null +++ b/deploy/docker/docker-compose.dev.yml @@ -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 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..4ee643c --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -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 diff --git a/deploy/k8s/app-deployment.yaml b/deploy/k8s/app-deployment.yaml new file mode 100644 index 0000000..06a3864 --- /dev/null +++ b/deploy/k8s/app-deployment.yaml @@ -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 diff --git a/deploy/k8s/app-service.yaml b/deploy/k8s/app-service.yaml new file mode 100644 index 0000000..a9ebafe --- /dev/null +++ b/deploy/k8s/app-service.yaml @@ -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 diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..7bcbdc7 --- /dev/null +++ b/scripts/common.sh @@ -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 diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh new file mode 100755 index 0000000..26281a5 --- /dev/null +++ b/scripts/docker-build.sh @@ -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" diff --git a/scripts/docker-cleanup.sh b/scripts/docker-cleanup.sh new file mode 100755 index 0000000..20c1480 --- /dev/null +++ b/scripts/docker-cleanup.sh @@ -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 λ¦¬μ†ŒμŠ€ 정리 μ™„λ£Œ!" diff --git a/scripts/k8s-cleanup.sh b/scripts/k8s-cleanup.sh new file mode 100755 index 0000000..7e8ffa3 --- /dev/null +++ b/scripts/k8s-cleanup.sh @@ -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 "βœ… 정리 μž‘μ—… μ™„λ£Œ" diff --git a/scripts/k8s-deploy.sh b/scripts/k8s-deploy.sh new file mode 100755 index 0000000..32362c2 --- /dev/null +++ b/scripts/k8s-deploy.sh @@ -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-)" + 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 배포 μž‘μ—… μ™„λ£Œ" diff --git a/services/nextjs/.eslintrc.json b/services/nextjs/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/services/nextjs/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/services/nextjs/README.md b/services/nextjs/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/services/nextjs/README.md @@ -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. diff --git a/services/nextjs/app/(movies)/movies/[id]/error.tsx b/services/nextjs/app/(movies)/movies/[id]/error.tsx new file mode 100644 index 0000000..6ccbc4e --- /dev/null +++ b/services/nextjs/app/(movies)/movies/[id]/error.tsx @@ -0,0 +1,5 @@ +"use client" + +export default function Error(){ + return

Something broke...

+} \ No newline at end of file diff --git a/services/nextjs/app/(movies)/movies/[id]/loading.tsx b/services/nextjs/app/(movies)/movies/[id]/loading.tsx new file mode 100644 index 0000000..0a1e768 --- /dev/null +++ b/services/nextjs/app/(movies)/movies/[id]/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return

Loading a movie :id

; +} diff --git a/services/nextjs/app/(movies)/movies/[id]/page.tsx b/services/nextjs/app/(movies)/movies/[id]/page.tsx new file mode 100644 index 0000000..6a85e52 --- /dev/null +++ b/services/nextjs/app/(movies)/movies/[id]/page.tsx @@ -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 ( +
+ Loading movie info}> + + + Loading videos info}> + + +
+ ); +} diff --git a/services/nextjs/app/about-us/page.tsx b/services/nextjs/app/about-us/page.tsx new file mode 100644 index 0000000..3199f70 --- /dev/null +++ b/services/nextjs/app/about-us/page.tsx @@ -0,0 +1,3 @@ +export default function AboutUs(){ + return
gfdgd
+} \ No newline at end of file diff --git a/services/nextjs/app/favicon.ico b/services/nextjs/app/favicon.ico new file mode 100644 index 0000000..df2deac Binary files /dev/null and b/services/nextjs/app/favicon.ico differ diff --git a/services/nextjs/app/layout.tsx b/services/nextjs/app/layout.tsx new file mode 100644 index 0000000..49025c7 --- /dev/null +++ b/services/nextjs/app/layout.tsx @@ -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 ( + + + + {children} + + + ); +} diff --git a/services/nextjs/app/page.tsx b/services/nextjs/app/page.tsx new file mode 100644 index 0000000..100e604 --- /dev/null +++ b/services/nextjs/app/page.tsx @@ -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 ( +
+ {movies.map((movie: any) => ( + + ))} +
+ ); +} diff --git a/services/nextjs/components/movie-info.tsx b/services/nextjs/components/movie-info.tsx new file mode 100644 index 0000000..f7b3d7c --- /dev/null +++ b/services/nextjs/components/movie-info.tsx @@ -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 ( +
+ {movie.title} +
+

{movie.title}

+

*{movie.vote_average.toFixed(1)}

+

{movie.overview}

+ + Homepage → + +
+
+ ); +} diff --git a/services/nextjs/components/movie-videos.tsx b/services/nextjs/components/movie-videos.tsx new file mode 100644 index 0000000..3589551 --- /dev/null +++ b/services/nextjs/components/movie-videos.tsx @@ -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 ( +
+ {videos.map((video: any) => ( +