commit b4ce36ba3bf82d4e0ed407cb22dfbdf7977609d7 Author: Mayne0213 Date: Sun Nov 30 10:44:12 2025 +0900 CHORE(app): init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c3d190 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,134 @@ +# 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 + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +*.md + +# Backup files +backups/ +*.backup +*.bak + +# Uploads (should be handled by volumes) +uploads/ + +# SSL certificates +ssl/ + +# Scripts +scripts/ +deploy/ + +# Prisma +prisma/migrations/ + +# TypeScript +*.tsbuildinfo + +# Testing +coverage/ +.nyc_output/ + +# Misc +.editorconfig +.prettierrc +.eslintrc* + +# Trunk +.trunk diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0e4cebb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,157 @@ +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: write + 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: Lowercase repository name + id: lowercase + run: | + echo "repo=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.repo }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}-sha-,format=long + 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: Extract SHA tag + id: extract-tag + run: | + # Extract the SHA-based tag from the tags list + TAGS="${{ steps.meta.outputs.tags }}" + echo "All tags:" + echo "$TAGS" + echo "---" + + # Get commit SHA (full 40 characters) + COMMIT_SHA="${{ github.sha }}" + + # Method 1: Extract the full SHA tag from docker/metadata-action output + # docker/metadata-action creates: main-sha- + SHA_TAG=$(echo "$TAGS" | grep -oE 'main-sha-[a-f0-9]{40}' | head -n 1) + + # Method 2: If not found, try to extract any main-sha- tag (fallback) + if [ -z "$SHA_TAG" ]; then + SHA_TAG=$(echo "$TAGS" | grep -oE 'main-sha-[a-f0-9]+' | head -n 1) + if [ -n "$SHA_TAG" ]; then + echo "⚠️ Found SHA tag (may not be full 40 chars): $SHA_TAG" + fi + fi + + # Method 3: Fallback to commit SHA directly (construct the tag) + if [ -z "$SHA_TAG" ]; then + SHA_TAG="main-sha-$COMMIT_SHA" + echo "⚠️ Could not extract from tags, using commit SHA: $SHA_TAG" + fi + + if [ -z "$SHA_TAG" ]; then + echo "❌ ERROR: Failed to extract SHA tag" + exit 1 + fi + + echo "sha-tag=$SHA_TAG" >> $GITHUB_OUTPUT + echo "✅ Extracted SHA tag: $SHA_TAG" + + - name: Update kustomization with new image tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + # Validate that SHA_TAG is not empty + SHA_TAG="${{ steps.extract-tag.outputs.sha-tag }}" + if [ -z "$SHA_TAG" ]; then + echo "❌ ERROR: SHA_TAG is empty, cannot update kustomization" + exit 1 + fi + + echo "📝 Updating kustomization.yaml with tag: $SHA_TAG" + + # Update kustomization.yaml with new image tag + # Handle both cases: newTag: (with value) and newTag: (empty) + sed -i.bak "s|newTag:.*|newTag: $SHA_TAG|" deploy/k8s/overlays/prod/kustomization.yaml + + # Verify the update was successful + if grep -q "newTag: $SHA_TAG" deploy/k8s/overlays/prod/kustomization.yaml; then + echo "✅ Successfully updated kustomization.yaml" + rm -f deploy/k8s/overlays/prod/kustomization.yaml.bak + else + echo "❌ ERROR: Failed to update kustomization.yaml" + cat deploy/k8s/overlays/prod/kustomization.yaml + exit 1 + fi + + # Commit and push if there are changes + if git diff --quiet; then + echo "No changes to commit" + else + git add deploy/k8s/overlays/prod/kustomization.yaml + git commit -m "Update image to $SHA_TAG" + git push + echo "✅ Kustomization updated with new image tag: $SHA_TAG" + fi + + - name: Display image information + run: | + echo "✅ Image built and pushed successfully!" + echo "📦 Image tags:" + echo "${{ steps.meta.outputs.tags }}" + echo "🔖 SHA tag: ${{ steps.extract-tag.outputs.sha-tag }}" + echo "🔖 Digest: ${{ steps.build.outputs.digest }}" + echo "" + echo "🚀 Kustomization updated with new image tag" + echo " ArgoCD will automatically detect and deploy this new image" + echo " Monitor deployment at your ArgoCD dashboard" 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/.gitignore b/.gitignore new file mode 100644 index 0000000..158c6c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +services/nextjs/node_modules +.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +coverage + +# next.js +services/nextjs/.next/ +.next/ +/out/ + +# production +/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 + +# prisma +/prisma/dev.db +/prisma/dev.db-journal +/prisma/*.db +/prisma/*.db-journal + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +Thumbs.db + +# trunk +.trunk diff --git a/deploy/argocd/application.yaml b/deploy/argocd/application.yaml new file mode 100644 index 0000000..254cf39 --- /dev/null +++ b/deploy/argocd/application.yaml @@ -0,0 +1,42 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: jaejadle + namespace: argocd + annotations: + argocd-image-updater.argoproj.io/image-list: jaejadle=ghcr.io/mayne0213/jaejadle + argocd-image-updater.argoproj.io/jaejadle.update-strategy: latest + argocd-image-updater.argoproj.io/write-back-method: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: https://github.com/Mayne0213/jaejadle.git + targetRevision: main + path: deploy/k8s/overlays/prod + + destination: + server: https://kubernetes.default.svc + namespace: jaejadle + + syncPolicy: + automated: + prune: true # 매니페스트에서 제거된 리소스 자동 삭제 + selfHeal: true # 클러스터에서 수동 변경 시 자동 복구 + allowEmpty: false + + syncOptions: + - CreateNamespace=true # namespace가 없으면 자동 생성 + - PrunePropagationPolicy=foreground + - PruneLast=true + + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + + revisionHistoryLimit: 10 diff --git a/deploy/docker/Dockerfile.dev b/deploy/docker/Dockerfile.dev new file mode 100644 index 0000000..211fada --- /dev/null +++ b/deploy/docker/Dockerfile.dev @@ -0,0 +1,29 @@ +# trunk-ignore-all(checkov/CKV_DOCKER_3) +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 . . + +# Generate Prisma Client +RUN npx prisma generate + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/api/health || exit 1 + +# Default command (can be overridden in docker-compose) +CMD ["npm", "run", "dev"] diff --git a/deploy/docker/Dockerfile.prod b/deploy/docker/Dockerfile.prod new file mode 100644 index 0000000..c158229 --- /dev/null +++ b/deploy/docker/Dockerfile.prod @@ -0,0 +1,65 @@ +# Multi-stage build for 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 +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 . . + +# Generate Prisma Client +# Build-time DATABASE_URL (not persisted in final image) +ARG DATABASE_URL="mysql://build:build@localhost:3306/build" +RUN npx prisma generate + +# 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 && adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next && 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 + +# Copy Prisma files +COPY --from=builder /app/prisma ./prisma + +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/api/health || 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..4d3e903 --- /dev/null +++ b/deploy/docker/docker-compose.dev.yml @@ -0,0 +1,64 @@ +services: + # Next.js Application (Development) - Using External Database + app: + image: jaejadle-app-dev + build: + context: ../../services/nextjs + dockerfile: ../../deploy/docker/Dockerfile.dev + container_name: jaejadle-app-dev + restart: unless-stopped + labels: + kompose.namespace: jaejadle-dev + ports: + - "3004:3000" + env_file: + - ../../.env + environment: + - NODE_ENV=development + networks: + - jaejadle-network-dev + volumes: + - ../../services/nextjs:/app + - /app/node_modules + - /app/.next + - app_logs_dev:/app/logs + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + command: > + sh -lc "npx prisma generate && npx prisma db push && npm run dev" + + # Prisma Studio - Connects to External Database + prisma-studio: + image: jaejadle-app-dev + container_name: jaejadle-prisma-studio + restart: unless-stopped + labels: + kompose.namespace: jaejadle-dev + ports: + - "5557:5555" + env_file: + - ../../.env + environment: + - NODE_ENV=development + networks: + - jaejadle-network-dev + volumes: + - ../../services/nextjs:/app + - /app/node_modules + command: npx prisma studio --port 5555 --hostname 0.0.0.0 + +volumes: + # Named volumes for data persistence + app_logs_dev: + driver: local + +networks: + jaejadle-network-dev: + driver: bridge + ipam: + config: + - subnet: 172.25.0.0/16 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..18d0c60 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,39 @@ +services: + # Next.js Application - Using External Database + app: + image: jaejadle-app + build: + context: ../../services/nextjs + dockerfile: ../../deploy/docker/Dockerfile.prod + container_name: jaejadle-app + restart: unless-stopped + labels: + kompose.namespace: jaejadle + ports: + - 3004:3000 + env_file: + - ../../.env + environment: + - NODE_ENV=production + networks: + - jaejadle-network + volumes: + - app_logs:/app/logs + healthcheck: + test: [CMD, curl, -f, http://localhost:3000/api/health] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + # Named volumes for data persistence + app_logs: + driver: local + +networks: + jaejadle-network: + driver: bridge + ipam: + config: + - subnet: 172.24.0.0/16 diff --git a/deploy/k8s/base/deployment.yaml b/deploy/k8s/base/deployment.yaml new file mode 100644 index 0000000..3e1dc1c --- /dev/null +++ b/deploy/k8s/base/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jaejadle-app + labels: + app: jaejadle-app +spec: + replicas: 1 + selector: + matchLabels: + app: jaejadle-app + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + template: + metadata: + labels: + app: jaejadle-app + spec: + containers: + - name: jaejadle-app + image: ghcr.io/mayne0213/jaejadle:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + protocol: TCP + env: + - name: NODE_ENV + value: production + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: jwt-secret + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: database-url + - name: AWS_REGION + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: aws-region + - name: AWS_S3_BUCKET_NAME + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: aws-s3-bucket-name + - name: AWS_S3_BUCKET_URL + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: aws-s3-bucket-url + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: aws-access-key-id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: jaejadle-secret + key: aws-secret-access-key + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "300m" + 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/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml new file mode 100644 index 0000000..0674fa8 --- /dev/null +++ b/deploy/k8s/base/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + +commonLabels: + app.kubernetes.io/name: jaejadle + app.kubernetes.io/component: web + +images: + - name: ghcr.io/mayne0213/jaejadle + newTag: latest diff --git a/deploy/k8s/base/service.yaml b/deploy/k8s/base/service.yaml new file mode 100644 index 0000000..c671009 --- /dev/null +++ b/deploy/k8s/base/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: jaejadle-service + labels: + app: jaejadle-app +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: 3000 + protocol: TCP + selector: + app: jaejadle-app diff --git a/deploy/k8s/overlays/prod/deployment-patch.yaml b/deploy/k8s/overlays/prod/deployment-patch.yaml new file mode 100644 index 0000000..f6f4685 --- /dev/null +++ b/deploy/k8s/overlays/prod/deployment-patch.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jaejadle-app + labels: + environment: production +spec: + replicas: 1 + template: + spec: + containers: + - name: jaejadle-app + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "300m" diff --git a/deploy/k8s/overlays/prod/kustomization.yaml b/deploy/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..d043e5c --- /dev/null +++ b/deploy/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: jaejadle + +resources: + - ../../base + - resourcequota.yaml + +commonLabels: + environment: production + +# 이미지 태그 설정 +images: + - name: ghcr.io/mayne0213/jaejadle + newTag: latest + +patchesStrategicMerge: + - deployment-patch.yaml diff --git a/deploy/k8s/overlays/prod/resourcequota.yaml b/deploy/k8s/overlays/prod/resourcequota.yaml new file mode 100644 index 0000000..4eb6953 --- /dev/null +++ b/deploy/k8s/overlays/prod/resourcequota.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + name: jaejadle-quota + namespace: jaejadle +spec: + hard: + requests.memory: "512Mi" + requests.cpu: "300m" + limits.memory: "1Gi" + limits.cpu: "600m" + pods: "3" diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..30b1a8c --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Jaejadle 스크립트 공통 유틸리티 함수들 +# 모든 Jaejadle 스크립트에서 사용할 수 있는 공통 기능들을 정의 + +set -e +# shopt -s inherit_errexit + +# 공통 스크립트의 절대 경로 기반 디렉토리 상수 +# 함수 호출 컨텍스트에 따라 BASH_SOURCE 해석이 달라질 수 있으므로 +# 로드 시점에 고정해 신뢰 가능한 루트를 계산한다 +JAEJADLE_SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JAEJADLE_ROOT="$(dirname "${JAEJADLE_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_jaejadle_root() { + # 로드 시점에 고정된 루트 경로 반환 + echo "${JAEJADLE_ROOT}" +} + +get_mayne_root() { + # jaejadle 루트를 기준으로 mayne 루트 경로 계산 + local jaejadle_root + jaejadle_root="$(get_jaejadle_root)" + dirname "$(dirname "${jaejadle_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 jaejadle_root + jaejadle_root="$(get_jaejadle_root)" + local dirs=("${@}") + + for dir in "${dirs[@]}"; do + if [[ ! -d "${jaejadle_root}/${dir}" ]]; then + log_error "필수 디렉토리가 없습니다: ${dir}" + exit 1 + fi + done +} + +# 필수 파일 확인 +check_required_files() { + local jaejadle_root + jaejadle_root="$(get_jaejadle_root)" + local files=("${@}") + + for file in "${files[@]}"; do + if [[ ! -f "${jaejadle_root}/${file}" ]]; then + log_error "필수 파일이 없습니다: ${file}" + exit 1 + fi + done +} + +# Docker 관련 유틸리티 +docker_cleanup_jaejadle() { + log_info "Jaejadle 관련 Docker 리소스 정리 중..." + + # 컨테이너 중지 및 삭제 + docker-compose -p jaejadle -f deploy/docker/docker-compose.yml down --remove-orphans 2>/dev/null || true + docker-compose -p jaejadle -f deploy/docker/docker-compose.dev.yml down --remove-orphans 2>/dev/null || true + docker ps -aq --filter "name=jaejadle" | xargs -r docker rm -f 2>/dev/null || true + + # 이미지 삭제 + docker images --filter "reference=jaejadle*" -q | xargs -r docker rmi -f 2>/dev/null || true + docker images --filter "reference=*jaejadle*" -q | xargs -r docker rmi -f 2>/dev/null || true + + # 볼륨 삭제 + docker volume ls -q --filter "name=jaejadle" | xargs -r docker volume rm -f 2>/dev/null || true + + # 시스템 정리 + docker system prune -f + + log_info "Docker 리소스 정리 완료" +} + +# 환경 변수 로드 +load_env_file() { + local jaejadle_root + jaejadle_root="$(get_jaejadle_root)" + local env_file="${jaejadle_root}/.env" + + if [[ -f "${env_file}" ]]; then + log_info "환경 변수 파일 로드: ${env_file}" + # shellcheck source=/dev/null + 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 + + # 스크립트 디렉토리로 이동 + local jaejadle_root + jaejadle_root="$(get_jaejadle_root)" + cd "${jaejadle_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..7994821 --- /dev/null +++ b/scripts/docker-build.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Jaejadle Docker 빌드 및 실행 스크립트 +# 공통 유틸리티 함수 로드 +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +# 스크립트 설정 +setup_script + +log_info "🚀 Jaejadle Docker 빌드 및 실행 시작..." + +# 필수 디렉토리 및 파일 확인 +log_info "📁 폴더 구조 확인 중..." +check_required_dirs "services/nextjs" "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 jaejadle -f docker-compose.dev.yml build --no-cache + docker-compose -p jaejadle -f docker-compose.dev.yml up -d + JAEJADLE_ROOT=$(get_jaejadle_root) + cd "${JAEJADLE_ROOT}" + ENV_TYPE="development" + COMPOSE_FILE_PATH="deploy/docker/docker-compose.dev.yml" + ;; + 2) + log_info "🏭 프로덕션 환경 빌드 및 실행 중..." + cd deploy/docker + docker-compose -p jaejadle -f docker-compose.yml build --no-cache + docker-compose -p jaejadle -f docker-compose.yml up -d + JAEJADLE_ROOT=$(get_jaejadle_root) + cd "${JAEJADLE_ROOT}" + ENV_TYPE="production" + COMPOSE_FILE_PATH="deploy/docker/docker-compose.yml" + ;; + 3) + log_info "🔨 이미지 빌드만 실행 중..." + cd deploy/docker + log_info " - 개발 이미지 빌드 중..." + docker-compose -p jaejadle -f docker-compose.dev.yml build --no-cache + log_info " - 프로덕션 이미지 빌드 중..." + docker-compose -p jaejadle -f docker-compose.yml build --no-cache + JAEJADLE_ROOT=$(get_jaejadle_root) + cd "${JAEJADLE_ROOT}" + log_info "✅ 빌드 완료! 실행하려면 다시 이 스크립트를 실행하고 환경을 선택하세요." + exit 0 + ;; + *) + log_error "잘못된 선택입니다." + exit 1 + ;; +esac + +# 서비스 상태 확인 +echo "" +log_info "⏳ 서비스 시작 대기 중..." +sleep 10 + +echo "" +log_info "📊 서비스 상태 확인:" +docker-compose -p jaejadle -f "${COMPOSE_FILE_PATH}" ps + +echo "" +log_info "🔍 컨테이너 로그 확인:" +echo " - 애플리케이션 로그: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} logs -f app" +if [[ "${ENV_TYPE}" = "development" ]]; then + echo " - Prisma Studio 로그: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} logs -f prisma-studio" +fi + +echo "" +log_info "🌐 접속 URL:" +if [[ "${ENV_TYPE}" = "development" ]]; then + echo " - 애플리케이션: http://localhost:3004" + echo " - Prisma Studio: http://localhost:5557" + echo " - 데이터베이스: 외부 DB" +else + echo " - 애플리케이션: http://localhost:3004" + echo " - 데이터베이스: 외부 DB" +fi + +echo "" +log_info "✅ Docker 빌드 및 실행 완료!" +echo "" +log_info "📋 유용한 명령어:" +echo " - 서비스 중지: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} down" +echo " - 로그 확인: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} logs -f" +echo " - 서비스 재시작: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} restart" +echo " - Prisma 마이그레이션: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} exec app npx prisma migrate deploy" +echo " - Prisma 스키마 동기화: docker-compose -p jaejadle -f ${COMPOSE_FILE_PATH} exec app npx prisma db push" +echo "" diff --git a/scripts/docker-cleanup.sh b/scripts/docker-cleanup.sh new file mode 100755 index 0000000..4631636 --- /dev/null +++ b/scripts/docker-cleanup.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Jaejadle Docker 리소스 정리 스크립트 +# 공통 유틸리티 함수 로드 +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +# 스크립트 설정 +setup_script + +log_info "🧹 Jaejadle Docker 리소스 정리 시작..." +log_info "💡 참고: 외부 DB를 사용하므로 데이터베이스 데이터는 보존됩니다." + +# 현재 실행 중인 Jaejadle 관련 컨테이너 확인 +log_info "📋 현재 실행 중인 Jaejadle 관련 컨테이너:" +docker ps -a --filter "name=jaejadle" --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" + +echo "" +if confirm_action "⚠️ 모든 Jaejadle 관련 컨테이너, 이미지, 볼륨을 삭제하시겠습니까?"; then + : # 계속 진행 +else + log_info "작업이 취소되었습니다." + exit 0 +fi + +echo "" +log_info "🛑 컨테이너 중지 및 삭제 중..." + +# Docker 정리 실행 +docker_cleanup_jaejadle + +log_info "✅ 정리 완료!" +echo "" +log_info "📊 정리된 리소스:" +echo " - Jaejadle 관련 컨테이너: 삭제됨" +echo " - Jaejadle 관련 이미지: 삭제됨" +echo " - Jaejadle 관련 볼륨: 삭제됨 (로그 파일)" +echo " - 사용하지 않는 Docker 리소스: 정리됨" +echo "" +log_info "💾 데이터베이스: 외부 DB에 안전하게 보존됨" +echo "" +log_info "🚀 이제 './scripts/docker-build.sh' 스크립트를 실행하여 재빌드하세요!" diff --git a/services/nextjs b/services/nextjs new file mode 160000 index 0000000..9f9ff75 --- /dev/null +++ b/services/nextjs @@ -0,0 +1 @@ +Subproject commit 9f9ff75d3ddd86030f6372483af30d5c1940312e