INIT(app): initial commit

- Initialize project structure
- Add base configuration
This commit is contained in:
2025-11-22 23:44:51 +09:00
commit 3c10907a97
41 changed files with 7170 additions and 0 deletions

70
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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

View 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"]

View 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"]

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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 배포 작업 완료"

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
services/nextjs/README.md Normal file
View 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.

View File

@@ -0,0 +1,5 @@
"use client"
export default function Error(){
return <h1>Something broke...</h1>
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return <h2>Loading a movie :id</h2>;
}

View 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>
);
}

View File

@@ -0,0 +1,3 @@
export default function AboutUs(){
return <div>gfdgd</div>
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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>
);
}

View 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>
);
}

View 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 &rarr;
</a>
</div>
</div>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View 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;
}

View File

@@ -0,0 +1,8 @@
.container {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 25px;
max-width: 90%;
width: 100%;
margin: 0 auto;
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View 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;

View 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"]
}