From 6d628427a60fdb6ace1327b48e8dfd4d9fdcf098 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 16:51:42 +0900 Subject: [PATCH 01/36] feat: start deployment --- deployment/Jenkinsfile | 140 ++++++++++++++++++ deployment/Jenkinsfile_ArgoCD | 136 +++++++++++++++++ .../container/Dockerfile-smarketing-frontend | 82 ++++++++++ deployment/container/nginx.conf | 69 +++++++++ deployment/deploy.yaml.template | 86 +++++++++++ deployment/deploy_env_vars | 34 +++++ deployment/manifest/deployment.yaml | 52 +++++++ deployment/manifest/frontend-configmap.yaml | 74 +++++++++ deployment/manifest/service.yaml | 15 ++ 9 files changed, 688 insertions(+) create mode 100644 deployment/Jenkinsfile create mode 100644 deployment/Jenkinsfile_ArgoCD create mode 100644 deployment/container/Dockerfile-smarketing-frontend create mode 100644 deployment/container/nginx.conf create mode 100644 deployment/deploy.yaml.template create mode 100644 deployment/deploy_env_vars create mode 100644 deployment/manifest/deployment.yaml create mode 100644 deployment/manifest/frontend-configmap.yaml create mode 100644 deployment/manifest/service.yaml diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile new file mode 100644 index 0000000..097683b --- /dev/null +++ b/deployment/Jenkinsfile @@ -0,0 +1,140 @@ +pipeline { + agent { + kubernetes { + yaml """ +apiVersion: v1 +kind: Pod +spec: + containers: + - name: podman + image: quay.io/podman/stable:latest + command: + - cat + tty: true + securityContext: + privileged: true + - name: envsubst + image: bhgedigital/envsubst + command: + - cat + tty: true + - name: azure-cli + image: mcr.microsoft.com/azure-cli:latest + command: + - cat + tty: true +""" + } + } + + environment { + // 빌드 정보 + imageTag = sh(script: "echo ${BUILD_NUMBER}", returnStdout: true).trim() + manifest = "deploy.yaml" + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Load Deploy Variables') { + steps { + script { + // deploy_env_vars 파일에서 환경변수 로드 + if (fileExists('deployment/deploy_env_vars')) { + def envVars = readFile('deploy_env_vars').trim() + envVars.split('\n').each { line -> + if (line.contains('=')) { + def (key, value) = line.split('=', 2) + env."${key}" = value + echo "Loaded: ${key} = ${value}" + } + } + } else { + error "deploy_env_vars 파일이 없습니다!" + } + + // 이미지 경로 설정 + env.imagePath = "${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:${imageTag}" + echo "Image Path: ${env.imagePath}" + } + } + } + + stage('Build & Push Image') { + container('podman') { + steps { + script { + sh """ + podman build \\ + --build-arg PROJECT_FOLDER="." \\ + --build-arg REACT_APP_AUTH_URL="${env.AUTH_URL}" \\ + --build-arg REACT_APP_MEMBER_URL="${env.MEMBER_URL}" \\ + --build-arg REACT_APP_STORE_URL="${env.STORE_URL}" \\ + --build-arg REACT_APP_CONTENT_URL="${env.CONTENT_URL}" \\ + --build-arg REACT_APP_RECOMMEND_URL="${env.RECOMMEND_URL}" \\ + --build-arg BUILD_FOLDER="deployment/container" \\ + --build-arg EXPORT_PORT="${env.EXPORT_PORT}" \\ + -f deployment/container/Dockerfile-smarketing-frontend \\ + -t ${env.imagePath} . + + podman push ${env.imagePath} + """ + } + } + } + } + + stage('Generate & Apply Manifest') { + container('envsubst') { + steps { + sh """ + export namespace=${env.NAMESPACE} + export smarketing_frontend_image_path=${env.imagePath} + export replicas=${env.REPLICAS} + export export_port=${env.EXPORT_PORT} + export ingress_host=${env.INGRESS_HOST} + export resources_requests_cpu=${env.RESOURCES_REQUESTS_CPU} + export resources_requests_memory=${env.RESOURCES_REQUESTS_MEMORY} + export resources_limits_cpu=${env.RESOURCES_LIMITS_CPU} + export resources_limits_memory=${env.RESOURCES_LIMITS_MEMORY} + + envsubst < deployment/${manifest}.template > deployment/${manifest} + echo "Generated manifest file:" + cat deployment/${manifest} + """ + } + } + + container('azure-cli') { + steps { + sh """ + kubectl apply -f deployment/${manifest} + + echo "Waiting for deployment to be ready..." + kubectl -n ${env.NAMESPACE} wait --for=condition=available deployment/smarketing-frontend --timeout=300s + + echo "Deployment completed successfully!" + kubectl -n ${env.NAMESPACE} get pods -l app=smarketing-frontend + kubectl -n ${env.NAMESPACE} get svc smarketing-frontend-service + """ + } + } + } + } + + post { + always { + cleanWs() + } + success { + echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" + } + failure { + echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." + } + } +} \ No newline at end of file diff --git a/deployment/Jenkinsfile_ArgoCD b/deployment/Jenkinsfile_ArgoCD new file mode 100644 index 0000000..099dcc5 --- /dev/null +++ b/deployment/Jenkinsfile_ArgoCD @@ -0,0 +1,136 @@ +pipeline { + agent { + kubernetes { + yaml """ +apiVersion: v1 +kind: Pod +spec: + containers: + - name: podman + image: quay.io/podman/stable:latest + command: + - cat + tty: true + securityContext: + privileged: true + - name: git + image: alpine/git:latest + command: + - cat + tty: true +""" + } + } + + environment { + imageTag = sh(script: "echo ${BUILD_NUMBER}", returnStdout: true).trim() + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Load Deploy Variables') { + steps { + script { + // deploy_env_vars 파일에서 환경변수 로드 + if (fileExists('deploy_env_vars')) { + def envVars = readFile('deploy_env_vars').trim() + envVars.split('\n').each { line -> + if (line.contains('=')) { + def (key, value) = line.split('=', 2) + env."${key}" = value + echo "Loaded: ${key} = ${value}" + } + } + } else { + error "deploy_env_vars 파일이 없습니다!" + } + + // 이미지 경로 설정 + env.imagePath = "${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:${imageTag}" + echo "Image Path: ${env.imagePath}" + } + } + } + + stage('Build & Push Image') { + container('podman') { + steps { + script { + sh """ + podman build \\ + --build-arg PROJECT_FOLDER="." \\ + --build-arg REACT_APP_AUTH_URL="${env.AUTH_URL}" \\ + --build-arg REACT_APP_MEMBER_URL="${env.MEMBER_URL}" \\ + --build-arg REACT_APP_STORE_URL="${env.STORE_URL}" \\ + --build-arg REACT_APP_CONTENT_URL="${env.CONTENT_URL}" \\ + --build-arg REACT_APP_RECOMMEND_URL="${env.RECOMMEND_URL}" \\ + --build-arg BUILD_FOLDER="deployment/container" \\ + --build-arg EXPORT_PORT="${env.EXPORT_PORT}" \\ + -f deployment/container/Dockerfile-smarketing-frontend \\ + -t ${env.imagePath} . + + podman push ${env.imagePath} + """ + } + } + } + } + + stage('Update Manifest Repository') { + container('git') { + withCredentials([usernamePassword( + credentialsId: 'github-credentials-${env.TEAMID}', + usernameVariable: 'GIT_USERNAME', + passwordVariable: 'GIT_PASSWORD' + )]) { + steps { + sh """ + # Git 설정 + git config --global user.name "Jenkins" + git config --global user.email "jenkins@company.com" + + # Manifest Repository Clone + git clone https://\$GIT_USERNAME:\$GIT_PASSWORD@github.com/${env.GITHUB_ORG}/smarketing-manifest.git + cd smarketing-manifest + + # Frontend 이미지 태그 업데이트 + echo "Updating smarketing-frontend deployment with image tag: ${imageTag}" + + if [ -f "smarketing-frontend/deployment.yaml" ]; then + # 기존 이미지 태그를 새 태그로 교체 + sed -i "s|image: ${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:.*|image: ${env.imagePath}|g" smarketing-frontend/deployment.yaml + + echo "Updated frontend deployment file:" + cat smarketing-frontend/deployment.yaml | grep "image:" + else + echo "Warning: Frontend deployment file not found" + fi + + # 변경사항 커밋 및 푸시 + git add . + git commit -m "Update smarketing-frontend image tag to ${imageTag}" || echo "No changes to commit" + git push origin main + """ + } + } + } + } + } + + post { + always { + cleanWs() + } + success { + echo "✅ smarketing-frontend 이미지 빌드 및 Manifest 업데이트가 완료되었습니다!" + } + failure { + echo "❌ smarketing-frontend CI/CD 파이프라인 중 오류가 발생했습니다." + } + } +} \ No newline at end of file diff --git a/deployment/container/Dockerfile-smarketing-frontend b/deployment/container/Dockerfile-smarketing-frontend new file mode 100644 index 0000000..1f688b7 --- /dev/null +++ b/deployment/container/Dockerfile-smarketing-frontend @@ -0,0 +1,82 @@ +# Build stage +FROM node:20-slim AS builder +ARG PROJECT_FOLDER + +ENV NODE_ENV=production + +WORKDIR /app + +# Install dependencies +COPY ${PROJECT_FOLDER}/package*.json ./ +RUN npm ci --only=production + +# Build application +COPY ${PROJECT_FOLDER} . +RUN npm run build + +# Run stage +FROM nginx:stable-alpine + +ARG BUILD_FOLDER +ARG EXPORT_PORT +ARG REACT_APP_AUTH_URL +ARG REACT_APP_MEMBER_URL +ARG REACT_APP_STORE_URL +ARG REACT_APP_CONTENT_URL +ARG REACT_APP_RECOMMEND_URL + +# Create nginx user if it doesn't exist +RUN adduser -S nginx || true + +# Copy build files +COPY --from=builder /app/build /usr/share/nginx/html + +# Create runtime config with all smarketing APIs +# index.html의 헤더에서 이 값을 읽어 환경변수를 생성함 +# api.js에서 이 환경변수를 이용함 +RUN echo "console.log('=== RUNTIME-ENV.JS 로드됨 (Docker 빌드) ==='); \ +window.__runtime_config__ = { \ + AUTH_URL: '${REACT_APP_AUTH_URL}', \ + MEMBER_URL: '${REACT_APP_MEMBER_URL}', \ + STORE_URL: '${REACT_APP_STORE_URL}', \ + CONTENT_URL: '${REACT_APP_CONTENT_URL}', \ + RECOMMEND_URL: '${REACT_APP_RECOMMEND_URL}', \ + ENV: 'production', \ + DEBUG: false, \ + API_TIMEOUT: 30000, \ + RETRY_ATTEMPTS: 3, \ + RETRY_DELAY: 1000, \ + VERSION: '1.0.0', \ + BUILD_DATE: new Date().toISOString() \ +}; \ +window.getApiConfig = () => window.__runtime_config__; \ +window.getApiUrl = (serviceName) => { \ + const config = window.__runtime_config__; \ + const urlKey = \`\${serviceName.toUpperCase()}_URL\`; \ + return config[urlKey] || null; \ +}; \ +console.log('✅ [RUNTIME] Docker 빌드 런타임 설정 로드 완료');" > /usr/share/nginx/html/runtime-env.js + +# Copy and process nginx configuration +COPY ${BUILD_FOLDER}/nginx.conf /etc/nginx/templates/default.conf.template + +# Add custom nginx settings +RUN echo "client_max_body_size 100M;" > /etc/nginx/conf.d/client_max_body_size.conf +RUN echo "proxy_buffer_size 128k;" > /etc/nginx/conf.d/proxy_buffer_size.conf +RUN echo "proxy_buffers 4 256k;" > /etc/nginx/conf.d/proxy_buffers.conf +RUN echo "proxy_busy_buffers_size 256k;" > /etc/nginx/conf.d/proxy_busy_buffers_size.conf + +# Set permissions +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chmod -R 755 /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid + +USER nginx + +EXPOSE ${EXPORT_PORT} + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/deployment/container/nginx.conf b/deployment/container/nginx.conf new file mode 100644 index 0000000..1ea0539 --- /dev/null +++ b/deployment/container/nginx.conf @@ -0,0 +1,69 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # Basic settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 10240; + gzip_proxied expired no-cache no-store private must-revalidate; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/x-javascript + application/xml+rss + application/javascript + application/json; + + server { + listen 18080; + server_name localhost; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} \ No newline at end of file diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template new file mode 100644 index 0000000..d572c21 --- /dev/null +++ b/deployment/deploy.yaml.template @@ -0,0 +1,86 @@ +# ConfigMap +apiVersion: v1 +kind: ConfigMap +metadata: + name: smarketing-frontend-config + namespace: ${namespace} +data: + runtime-env.js: | + window.__runtime_config__ = { + AUTH_URL: 'http://${ingress_host}/auth', + CONTENT_URL: 'http://${ingress_host}/content', + AI_URL: 'http://${ingress_host}/ai' + }; + +--- +# Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: smarketing-frontend + namespace: ${namespace} + labels: + app: smarketing-frontend +spec: + replicas: ${replicas} + selector: + matchLabels: + app: smarketing-frontend + template: + metadata: + labels: + app: smarketing-frontend + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: smarketing-frontend + image: ${smarketing_frontend_image_path} + imagePullPolicy: Always + ports: + - containerPort: ${export_port} + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + volumeMounts: + - name: runtime-config + mountPath: /usr/share/nginx/html/runtime-env.js + subPath: runtime-env.js + livenessProbe: + httpGet: + path: /health + port: ${export_port} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: ${export_port} + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: runtime-config + configMap: + name: smarketing-frontend-config + +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: smarketing-frontend-service + namespace: ${namespace} + labels: + app: smarketing-frontend +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: ${export_port} + protocol: TCP + selector: + app: smarketing-frontend \ No newline at end of file diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars new file mode 100644 index 0000000..14d772a --- /dev/null +++ b/deployment/deploy_env_vars @@ -0,0 +1,34 @@ +# smarketing-frontend 배포 환경변수 설정 +# 이 파일을 실제 환경에 맞게 수정하세요 + +# Container Registry 설정 +REGISTRY=acrdigitalgarage02.azurecr.io +IMAGE_ORG=smarketing + +# Kubernetes 설정 +NAMESPACE=smarketing +REPLICAS=1 +EXPORT_PORT=18080 + +# Gateway/Ingress 설정 (⭐ smarketing-backend와 동일한 IP 사용) +INGRESS_HOST=smarketing.20.249.184.228.nip.io + +# 리소스 설정 +RESOURCES_REQUESTS_CPU=256m +RESOURCES_REQUESTS_MEMORY=256Mi +RESOURCES_LIMITS_CPU=1024m +RESOURCES_LIMITS_MEMORY=1024Mi + +# API URLs (⭐ smarketing-backend ingress를 통해 라우팅) +# 현재 설정된 백엔드 API들과 일치 +AUTH_URL=http://smarketing.20.249.184.228.nip.io/api/auth +MEMBER_URL=http://smarketing.20.249.184.228.nip.io/api/member +STORE_URL=http://smarketing.20.249.184.228.nip.io/api/store +MENU_URL=http://smarketing.20.249.184.228.nip.io/api/menu +SALES_URL=http://smarketing.20.249.184.228.nip.io/api/sales +CONTENT_URL=http://smarketing.20.249.184.228.nip.io/api/content +RECOMMEND_URL=http://smarketing.20.249.184.228.nip.io/api/recommend + +# GitHub 설정 (실제 팀 설정으로 변경) +GITHUB_ORG=won-ktds +TEAMID=smarketing \ No newline at end of file diff --git a/deployment/manifest/deployment.yaml b/deployment/manifest/deployment.yaml new file mode 100644 index 0000000..272d253 --- /dev/null +++ b/deployment/manifest/deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: smarketing-frontend + namespace: ${namespace} + labels: + app: smarketing-frontend +spec: + replicas: ${replicas} + selector: + matchLabels: + app: smarketing-frontend + template: + metadata: + labels: + app: smarketing-frontend + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: smarketing-frontend + image: ${smarketing_frontend_image_path} + imagePullPolicy: Always + ports: + - containerPort: ${export_port} + resources: + requests: + cpu: ${resources_requests_cpu} + memory: ${resources_requests_memory} + limits: + cpu: ${resources_limits_cpu} + memory: ${resources_limits_memory} + volumeMounts: + - name: runtime-config + mountPath: /usr/share/nginx/html/runtime-env.js + subPath: runtime-env.js + livenessProbe: + httpGet: + path: /health + port: ${export_port} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: ${export_port} + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: runtime-config + configMap: + name: smarketing-frontend-config \ No newline at end of file diff --git a/deployment/manifest/frontend-configmap.yaml b/deployment/manifest/frontend-configmap.yaml new file mode 100644 index 0000000..d111f6e --- /dev/null +++ b/deployment/manifest/frontend-configmap.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: smarketing-frontend-config + namespace: ${namespace} +data: + runtime-env.js: | + console.log('=== RUNTIME-ENV.JS 로드됨 (배포 환경) ==='); + + window.__runtime_config__ = { + // 백엔드 API 구조에 맞게 URL 설정 + AUTH_URL: 'http://${ingress_host}/api/auth', + MEMBER_URL: 'http://${ingress_host}/api/member', + STORE_URL: 'http://${ingress_host}/api/store', + MENU_URL: 'http://${ingress_host}/api/menu', + SALES_URL: 'http://${ingress_host}/api/sales', + CONTENT_URL: 'http://${ingress_host}/api/content', + RECOMMEND_URL: 'http://${ingress_host}/api/recommendations', + + // Gateway URL (운영 환경) + GATEWAY_URL: 'http://${ingress_host}', + + // 기능 플래그 + FEATURES: { + ANALYTICS: true, + PUSH_NOTIFICATIONS: true, + SOCIAL_LOGIN: false, + MULTI_LANGUAGE: false, + API_HEALTH_CHECK: true, + }, + + // 환경 설정 (배포 환경) + ENV: 'production', + DEBUG: false, + + // API 타임아웃 설정 + API_TIMEOUT: 30000, + + // 재시도 설정 + RETRY_ATTEMPTS: 3, + RETRY_DELAY: 1000, + + // 버전 정보 + VERSION: '1.0.0', + BUILD_DATE: new Date().toISOString() + }; + + // 설정 검증 함수 + const validateConfig = () => { + const config = window.__runtime_config__; + const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL']; + + for (const url of requiredUrls) { + if (!config[url]) { + console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`); + return false; + } + } + + console.log('✅ [CONFIG] 설정 검증 완료'); + return true; + }; + + // 전역 설정 접근 함수 + window.getApiConfig = () => window.__runtime_config__; + window.getApiUrl = (serviceName) => { + const config = window.__runtime_config__; + const urlKey = `${serviceName.toUpperCase()}_URL`; + return config[urlKey] || null; + }; + + // 설정 검증 실행 + validateConfig(); + console.log('✅ [RUNTIME] 런타임 설정 로드 완료 (배포 환경)'); \ No newline at end of file diff --git a/deployment/manifest/service.yaml b/deployment/manifest/service.yaml new file mode 100644 index 0000000..fcb845a --- /dev/null +++ b/deployment/manifest/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: smarketing-frontend-service + namespace: ${namespace} + labels: + app: smarketing-frontend +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: ${export_port} + protocol: TCP + selector: + app: smarketing-frontend \ No newline at end of file From 3549254b8bac4e72e259d3989c882d6c3ccc8095 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 16:54:34 +0900 Subject: [PATCH 02/36] Fix Jenkins pipeline syntax error --- deployment/Jenkinsfile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 097683b..268eac4 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -44,7 +44,7 @@ spec: steps { script { // deploy_env_vars 파일에서 환경변수 로드 - if (fileExists('deployment/deploy_env_vars')) { + if (fileExists('deploy_env_vars')) { def envVars = readFile('deploy_env_vars').trim() envVars.split('\n').each { line -> if (line.contains('=')) { @@ -65,8 +65,8 @@ spec: } stage('Build & Push Image') { - container('podman') { - steps { + steps { + container('podman') { script { sh """ podman build \\ @@ -89,8 +89,8 @@ spec: } stage('Generate & Apply Manifest') { - container('envsubst') { - steps { + steps { + container('envsubst') { sh """ export namespace=${env.NAMESPACE} export smarketing_frontend_image_path=${env.imagePath} @@ -107,10 +107,8 @@ spec: cat deployment/${manifest} """ } - } - - container('azure-cli') { - steps { + + container('azure-cli') { sh """ kubectl apply -f deployment/${manifest} From 1378397226560c5ab39583bb0a9bb594df9955f2 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 16:57:00 +0900 Subject: [PATCH 03/36] fix: Jenkinsfile --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 268eac4..08b19e7 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -132,7 +132,7 @@ spec: echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" } failure { - echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." + echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다.." } } } \ No newline at end of file From 05bebaee755320ac492ec7d06dea44d33413d6cc Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:00:48 +0900 Subject: [PATCH 04/36] refactor: env_vars: --- deployment/deploy_env_vars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars index 14d772a..8926d64 100644 --- a/deployment/deploy_env_vars +++ b/deployment/deploy_env_vars @@ -29,6 +29,6 @@ SALES_URL=http://smarketing.20.249.184.228.nip.io/api/sales CONTENT_URL=http://smarketing.20.249.184.228.nip.io/api/content RECOMMEND_URL=http://smarketing.20.249.184.228.nip.io/api/recommend -# GitHub 설정 (실제 팀 설정으로 변경) +# GitHub 설정 GITHUB_ORG=won-ktds TEAMID=smarketing \ No newline at end of file From 93ddac2c7df8117c886e2adeff09295bcc08ff78 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:03:31 +0900 Subject: [PATCH 05/36] refactor: deployment --- deployment/Jenkinsfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 08b19e7..5df231f 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -65,8 +65,8 @@ spec: } stage('Build & Push Image') { - steps { - container('podman') { + container('podman') { + steps { script { sh """ podman build \\ @@ -89,8 +89,8 @@ spec: } stage('Generate & Apply Manifest') { - steps { - container('envsubst') { + container('envsubst') { + steps { sh """ export namespace=${env.NAMESPACE} export smarketing_frontend_image_path=${env.imagePath} @@ -107,8 +107,10 @@ spec: cat deployment/${manifest} """ } - - container('azure-cli') { + } + + container('azure-cli') { + steps { sh """ kubectl apply -f deployment/${manifest} @@ -132,7 +134,7 @@ spec: echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" } failure { - echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다.." + echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." } } } \ No newline at end of file From 1e9874461c66800734d30d88974104178009d255 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:05:46 +0900 Subject: [PATCH 06/36] refactor: deployment --- deployment/Jenkinsfile_ArgoCD | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deployment/Jenkinsfile_ArgoCD b/deployment/Jenkinsfile_ArgoCD index 099dcc5..0c6aec3 100644 --- a/deployment/Jenkinsfile_ArgoCD +++ b/deployment/Jenkinsfile_ArgoCD @@ -58,8 +58,8 @@ spec: } stage('Build & Push Image') { - container('podman') { - steps { + steps { + container('podman') { script { sh """ podman build \\ @@ -82,13 +82,13 @@ spec: } stage('Update Manifest Repository') { - container('git') { - withCredentials([usernamePassword( - credentialsId: 'github-credentials-${env.TEAMID}', - usernameVariable: 'GIT_USERNAME', - passwordVariable: 'GIT_PASSWORD' - )]) { - steps { + steps { + container('git') { + withCredentials([usernamePassword( + credentialsId: 'github-credentials-${env.TEAMID}', + usernameVariable: 'GIT_USERNAME', + passwordVariable: 'GIT_PASSWORD' + )]) { sh """ # Git 설정 git config --global user.name "Jenkins" From eb6426c9251128edb910ab47c6c8de8a2f803a97 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:08:12 +0900 Subject: [PATCH 07/36] test: webhook trigger From 153cce0de62f2aff14cc5f364c743429b25f26df Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:11:29 +0900 Subject: [PATCH 08/36] refactor: deployment --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 5df231f..70192a9 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -134,7 +134,7 @@ spec: echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" } failure { - echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." + echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다..." } } } \ No newline at end of file From c2298b081c12dadcbb6f2c1d2808d1a992b12d47 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:14:19 +0900 Subject: [PATCH 09/36] refactor: deployment --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 70192a9..5df231f 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -134,7 +134,7 @@ spec: echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" } failure { - echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다..." + echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." } } } \ No newline at end of file From 7292d993359ce46f810c2e5eafee094b3d722102 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:21:28 +0900 Subject: [PATCH 10/36] fix: Jenkinsfile --- deployment/Jenkinsfile | 233 ++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 121 deletions(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 5df231f..80cb57f 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -1,140 +1,131 @@ -pipeline { - agent { - kubernetes { - yaml """ -apiVersion: v1 -kind: Pod -spec: - containers: - - name: podman - image: quay.io/podman/stable:latest - command: - - cat - tty: true - securityContext: - privileged: true - - name: envsubst - image: bhgedigital/envsubst - command: - - cat - tty: true - - name: azure-cli - image: mcr.microsoft.com/azure-cli:latest - command: - - cat - tty: true -""" +// deployment/Jenkinsfile +def PIPELINE_ID = "${env.BUILD_NUMBER}" + +def getImageTag() { + def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss') + def currentDate = new Date() + return dateFormat.format(currentDate) +} + +podTemplate( + label: "${PIPELINE_ID}", + serviceAccount: 'jenkins', + containers: [ + containerTemplate(name: 'node', image: 'node:20-slim', ttyEnabled: true, command: 'cat'), + containerTemplate(name: 'podman', image: "mgoltzsche/podman", ttyEnabled: true, command: 'cat', privileged: true), + containerTemplate(name: 'azure-cli', image: 'hiondal/azure-kubectl:latest', command: 'cat', ttyEnabled: true), + containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h') + ], + volumes: [ + emptyDirVolume(mountPath: '/root/.azure', memory: false), + emptyDirVolume(mountPath: '/run/podman', memory: false) + ] +) { + node(PIPELINE_ID) { + def props + def imageTag = getImageTag() + def manifest = "deploy.yaml" + def namespace + + stage("Get Source") { + checkout scm + props = readProperties file: "deployment/deploy_env_vars" + namespace = "${props.namespace}" + + echo "Registry: ${props.registry}" + echo "Image Org: ${props.image_org}" + echo "Image Tag: ${imageTag}" } - } - - environment { - // 빌드 정보 - imageTag = sh(script: "echo ${BUILD_NUMBER}", returnStdout: true).trim() - manifest = "deploy.yaml" - } - - stages { - stage('Checkout') { - steps { - checkout scm - } - } - - stage('Load Deploy Variables') { - steps { - script { - // deploy_env_vars 파일에서 환경변수 로드 - if (fileExists('deploy_env_vars')) { - def envVars = readFile('deploy_env_vars').trim() - envVars.split('\n').each { line -> - if (line.contains('=')) { - def (key, value) = line.split('=', 2) - env."${key}" = value - echo "Loaded: ${key} = ${value}" - } - } - } else { - error "deploy_env_vars 파일이 없습니다!" - } - - // 이미지 경로 설정 - env.imagePath = "${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:${imageTag}" - echo "Image Path: ${env.imagePath}" + + stage("Setup AKS") { + container('azure-cli') { + withCredentials([azureServicePrincipal('azure-credentials')]) { + sh """ + az login --service-principal -u \$AZURE_CLIENT_ID -p \$AZURE_CLIENT_SECRET -t \$AZURE_TENANT_ID + az aks get-credentials --resource-group rg-digitalgarage-02 --name aks-digitalgarage-02 --overwrite-existing + kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f - + """ } } } - + stage('Build & Push Image') { container('podman') { - steps { - script { - sh """ - podman build \\ - --build-arg PROJECT_FOLDER="." \\ - --build-arg REACT_APP_AUTH_URL="${env.AUTH_URL}" \\ - --build-arg REACT_APP_MEMBER_URL="${env.MEMBER_URL}" \\ - --build-arg REACT_APP_STORE_URL="${env.STORE_URL}" \\ - --build-arg REACT_APP_CONTENT_URL="${env.CONTENT_URL}" \\ - --build-arg REACT_APP_RECOMMEND_URL="${env.RECOMMEND_URL}" \\ - --build-arg BUILD_FOLDER="deployment/container" \\ - --build-arg EXPORT_PORT="${env.EXPORT_PORT}" \\ - -f deployment/container/Dockerfile-smarketing-frontend \\ - -t ${env.imagePath} . - - podman push ${env.imagePath} - """ - } + sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2' + + withCredentials([usernamePassword( + credentialsId: 'acr-credentials', + usernameVariable: 'ACR_USERNAME', + passwordVariable: 'ACR_PASSWORD' + )]) { + def imagePath = "${props.registry}/${props.image_org}/smarketing-frontend:${imageTag}" + + sh """ + echo "==========================================" + echo "Building smarketing-frontend" + echo "Image Tag: ${imageTag}" + echo "Image Path: ${imagePath}" + echo "==========================================" + + # ACR 로그인 + echo \$ACR_PASSWORD | podman login ${props.registry} --username \$ACR_USERNAME --password-stdin + + # Docker 이미지 빌드 + podman build \\ + --build-arg PROJECT_FOLDER="." \\ + --build-arg REACT_APP_AUTH_URL="${props.auth_url}" \\ + --build-arg REACT_APP_MEMBER_URL="${props.member_url}" \\ + --build-arg REACT_APP_STORE_URL="${props.store_url}" \\ + --build-arg REACT_APP_CONTENT_URL="${props.content_url}" \\ + --build-arg REACT_APP_RECOMMEND_URL="${props.recommend_url}" \\ + --build-arg BUILD_FOLDER="deployment/container" \\ + --build-arg EXPORT_PORT="${props.export_port}" \\ + -f deployment/container/Dockerfile-smarketing-frontend \\ + -t ${imagePath} . + + # 이미지 푸시 + podman push ${imagePath} + + echo "Image pushed successfully: ${imagePath}" + """ } } } stage('Generate & Apply Manifest') { container('envsubst') { - steps { - sh """ - export namespace=${env.NAMESPACE} - export smarketing_frontend_image_path=${env.imagePath} - export replicas=${env.REPLICAS} - export export_port=${env.EXPORT_PORT} - export ingress_host=${env.INGRESS_HOST} - export resources_requests_cpu=${env.RESOURCES_REQUESTS_CPU} - export resources_requests_memory=${env.RESOURCES_REQUESTS_MEMORY} - export resources_limits_cpu=${env.RESOURCES_LIMITS_CPU} - export resources_limits_memory=${env.RESOURCES_LIMITS_MEMORY} - - envsubst < deployment/${manifest}.template > deployment/${manifest} - echo "Generated manifest file:" - cat deployment/${manifest} - """ - } + def imagePath = "${props.registry}/${props.image_org}/smarketing-frontend:${imageTag}" + + sh """ + export namespace=${namespace} + export smarketing_frontend_image_path=${imagePath} + export replicas=${props.replicas} + export export_port=${props.export_port} + export ingress_host=${props.ingress_host} + export resources_requests_cpu=${props.resources_requests_cpu} + export resources_requests_memory=${props.resources_requests_memory} + export resources_limits_cpu=${props.resources_limits_cpu} + export resources_limits_memory=${props.resources_limits_memory} + + envsubst < deployment/${manifest}.template > deployment/${manifest} + echo "Generated manifest file:" + cat deployment/${manifest} + """ } container('azure-cli') { - steps { - sh """ - kubectl apply -f deployment/${manifest} - - echo "Waiting for deployment to be ready..." - kubectl -n ${env.NAMESPACE} wait --for=condition=available deployment/smarketing-frontend --timeout=300s - - echo "Deployment completed successfully!" - kubectl -n ${env.NAMESPACE} get pods -l app=smarketing-frontend - kubectl -n ${env.NAMESPACE} get svc smarketing-frontend-service - """ - } + sh """ + kubectl apply -f deployment/${manifest} + + echo "Waiting for deployment to be ready..." + kubectl -n ${namespace} wait --for=condition=available deployment/smarketing-frontend --timeout=300s + + echo "Deployment completed successfully!" + kubectl -n ${namespace} get pods -l app=smarketing-frontend + kubectl -n ${namespace} get svc smarketing-frontend-service + kubectl -n ${namespace} get ingress + """ } } } - - post { - always { - cleanWs() - } - success { - echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!" - } - failure { - echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다." - } - } } \ No newline at end of file From 282a810762e26734d99e0e1e6013c002930b3e2d Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:23:22 +0900 Subject: [PATCH 11/36] fix: Jenkinsfile --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 80cb57f..70fff6b 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -128,4 +128,4 @@ podTemplate( } } } -} \ No newline at end of file +} From 405376c7c300f57403f6f554a9adcfc8c3250643 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:26:06 +0900 Subject: [PATCH 12/36] Trigger CI/CD From 2652b3a54c0665257c356156222fb9d40ab6d60d Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:38:58 +0900 Subject: [PATCH 13/36] release --- deployment/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 70fff6b..e99a6ad 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -7,6 +7,7 @@ def getImageTag() { return dateFormat.format(currentDate) } + podTemplate( label: "${PIPELINE_ID}", serviceAccount: 'jenkins', From fbfcd04758b14175d9260d46b6c3d7321d59f5f6 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:48:24 +0900 Subject: [PATCH 14/36] fix: deployment --- deployment/deploy.yaml.template | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template index d572c21..e0de525 100644 --- a/deployment/deploy.yaml.template +++ b/deployment/deploy.yaml.template @@ -1,4 +1,3 @@ -# ConfigMap apiVersion: v1 kind: ConfigMap metadata: @@ -7,9 +6,11 @@ metadata: data: runtime-env.js: | window.__runtime_config__ = { - AUTH_URL: 'http://${ingress_host}/auth', - CONTENT_URL: 'http://${ingress_host}/content', - AI_URL: 'http://${ingress_host}/ai' + AUTH_URL: 'http://${ingress_host}/api/auth', + MEMBER_URL: 'http://${ingress_host}/api/member', + STORE_URL: 'http://${ingress_host}/api/store', + CONTENT_URL: 'http://${ingress_host}/api/content', + RECOMMEND_URL: 'http://${ingress_host}/api/recommend' }; --- @@ -52,16 +53,20 @@ spec: subPath: runtime-env.js livenessProbe: httpGet: - path: /health + path: / port: ${export_port} initialDelaySeconds: 30 periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 readinessProbe: httpGet: - path: /health + path: / port: ${export_port} initialDelaySeconds: 5 periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 volumes: - name: runtime-config configMap: @@ -77,10 +82,11 @@ metadata: labels: app: smarketing-frontend spec: - type: ClusterIP + type: LoadBalancer ports: - port: 80 targetPort: ${export_port} protocol: TCP + name: http selector: app: smarketing-frontend \ No newline at end of file From abb9e065d512caaddbcfdf0d6cbe352c730d6c1a Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:51:53 +0900 Subject: [PATCH 15/36] fix: deployment --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index e99a6ad..35ef7c0 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -30,7 +30,7 @@ podTemplate( stage("Get Source") { checkout scm - props = readProperties file: "deployment/deploy_env_vars" + props = readProperties file: "smarketing-frontent/deployment/deploy_env_vars" namespace = "${props.namespace}" echo "Registry: ${props.registry}" From 5d2c500ff3603240d339a49e2585a270ee4540f0 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:53:03 +0900 Subject: [PATCH 16/36] fix: deployment --- deployment/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 35ef7c0..e99a6ad 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -30,7 +30,7 @@ podTemplate( stage("Get Source") { checkout scm - props = readProperties file: "smarketing-frontent/deployment/deploy_env_vars" + props = readProperties file: "deployment/deploy_env_vars" namespace = "${props.namespace}" echo "Registry: ${props.registry}" From cf2765296c5abf5769b21fa26500da957ae88ff2 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 17:57:40 +0900 Subject: [PATCH 17/36] fix: deployment --- deployment/Jenkinsfile | 17 ++++++++++++-- deployment/deploy.yaml.template | 13 ++++++----- deployment/deploy_env_vars | 39 +++++++++++++++++---------------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index e99a6ad..9a3279d 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -7,7 +7,6 @@ def getImageTag() { return dateFormat.format(currentDate) } - podTemplate( label: "${PIPELINE_ID}", serviceAccount: 'jenkins', @@ -30,11 +29,23 @@ podTemplate( stage("Get Source") { checkout scm + + // 환경변수 파일 확인 및 읽기 + if (!fileExists('deployment/deploy_env_vars')) { + error "deployment/deploy_env_vars 파일이 없습니다!" + } + props = readProperties file: "deployment/deploy_env_vars" namespace = "${props.namespace}" + // 필수 환경변수 검증 + if (!props.registry || !props.image_org || !props.namespace) { + error "필수 환경변수가 누락되었습니다. registry, image_org, namespace를 확인하세요." + } + echo "Registry: ${props.registry}" echo "Image Org: ${props.image_org}" + echo "Namespace: ${namespace}" echo "Image Tag: ${imageTag}" } @@ -77,6 +88,8 @@ podTemplate( --build-arg REACT_APP_AUTH_URL="${props.auth_url}" \\ --build-arg REACT_APP_MEMBER_URL="${props.member_url}" \\ --build-arg REACT_APP_STORE_URL="${props.store_url}" \\ + --build-arg REACT_APP_MENU_URL="${props.menu_url}" \\ + --build-arg REACT_APP_SALES_URL="${props.sales_url}" \\ --build-arg REACT_APP_CONTENT_URL="${props.content_url}" \\ --build-arg REACT_APP_RECOMMEND_URL="${props.recommend_url}" \\ --build-arg BUILD_FOLDER="deployment/container" \\ @@ -129,4 +142,4 @@ podTemplate( } } } -} +} \ No newline at end of file diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template index e0de525..4e46d80 100644 --- a/deployment/deploy.yaml.template +++ b/deployment/deploy.yaml.template @@ -1,3 +1,4 @@ +# ConfigMap apiVersion: v1 kind: ConfigMap metadata: @@ -6,11 +7,13 @@ metadata: data: runtime-env.js: | window.__runtime_config__ = { - AUTH_URL: 'http://${ingress_host}/api/auth', - MEMBER_URL: 'http://${ingress_host}/api/member', - STORE_URL: 'http://${ingress_host}/api/store', - CONTENT_URL: 'http://${ingress_host}/api/content', - RECOMMEND_URL: 'http://${ingress_host}/api/recommend' + AUTH_URL: '${auth_url}', + MEMBER_URL: '${member_url}', + STORE_URL: '${store_url}', + MENU_URL: '${menu_url}', + SALES_URL: '${sales_url}', + CONTENT_URL: '${content_url}', + RECOMMEND_URL: '${recommend_url}' }; --- diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars index 8926d64..aa706ef 100644 --- a/deployment/deploy_env_vars +++ b/deployment/deploy_env_vars @@ -1,34 +1,35 @@ +# deployment/deploy_env_vars # smarketing-frontend 배포 환경변수 설정 # 이 파일을 실제 환경에 맞게 수정하세요 # Container Registry 설정 -REGISTRY=acrdigitalgarage02.azurecr.io -IMAGE_ORG=smarketing +registry=acrdigitalgarage02.azurecr.io +image_org=smarketing # Kubernetes 설정 -NAMESPACE=smarketing -REPLICAS=1 -EXPORT_PORT=18080 +namespace=smarketing +replicas=1 +export_port=18080 # Gateway/Ingress 설정 (⭐ smarketing-backend와 동일한 IP 사용) -INGRESS_HOST=smarketing.20.249.184.228.nip.io +ingress_host=smarketing.20.249.184.228.nip.io # 리소스 설정 -RESOURCES_REQUESTS_CPU=256m -RESOURCES_REQUESTS_MEMORY=256Mi -RESOURCES_LIMITS_CPU=1024m -RESOURCES_LIMITS_MEMORY=1024Mi +resources_requests_cpu=256m +resources_requests_memory=256Mi +resources_limits_cpu=1024m +resources_limits_memory=1024Mi # API URLs (⭐ smarketing-backend ingress를 통해 라우팅) # 현재 설정된 백엔드 API들과 일치 -AUTH_URL=http://smarketing.20.249.184.228.nip.io/api/auth -MEMBER_URL=http://smarketing.20.249.184.228.nip.io/api/member -STORE_URL=http://smarketing.20.249.184.228.nip.io/api/store -MENU_URL=http://smarketing.20.249.184.228.nip.io/api/menu -SALES_URL=http://smarketing.20.249.184.228.nip.io/api/sales -CONTENT_URL=http://smarketing.20.249.184.228.nip.io/api/content -RECOMMEND_URL=http://smarketing.20.249.184.228.nip.io/api/recommend +auth_url=http://smarketing.20.249.184.228.nip.io/api/auth +member_url=http://smarketing.20.249.184.228.nip.io/api/member +store_url=http://smarketing.20.249.184.228.nip.io/api/store +menu_url=http://smarketing.20.249.184.228.nip.io/api/menu +sales_url=http://smarketing.20.249.184.228.nip.io/api/sales +content_url=http://smarketing.20.249.184.228.nip.io/api/content +recommend_url=http://smarketing.20.249.184.228.nip.io/api/recommend # GitHub 설정 -GITHUB_ORG=won-ktds -TEAMID=smarketing \ No newline at end of file +github_org=won-ktds +teamid=smarketing \ No newline at end of file From e3ec0f6c166d45f2c15fe7cd6cd6e90e7c11d7b6 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 18:09:45 +0900 Subject: [PATCH 18/36] fix: deployment --- deployment/Jenkinsfile | 14 +-- .../container/Dockerfile-smarketing-frontend | 92 +++++++------------ package.json | 5 +- vite.config.js | 44 ++++----- 4 files changed, 63 insertions(+), 92 deletions(-) diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile index 9a3279d..d3649a7 100644 --- a/deployment/Jenkinsfile +++ b/deployment/Jenkinsfile @@ -85,13 +85,13 @@ podTemplate( # Docker 이미지 빌드 podman build \\ --build-arg PROJECT_FOLDER="." \\ - --build-arg REACT_APP_AUTH_URL="${props.auth_url}" \\ - --build-arg REACT_APP_MEMBER_URL="${props.member_url}" \\ - --build-arg REACT_APP_STORE_URL="${props.store_url}" \\ - --build-arg REACT_APP_MENU_URL="${props.menu_url}" \\ - --build-arg REACT_APP_SALES_URL="${props.sales_url}" \\ - --build-arg REACT_APP_CONTENT_URL="${props.content_url}" \\ - --build-arg REACT_APP_RECOMMEND_URL="${props.recommend_url}" \\ + --build-arg VUE_APP_AUTH_URL="${props.auth_url}" \\ + --build-arg VUE_APP_MEMBER_URL="${props.member_url}" \\ + --build-arg VUE_APP_STORE_URL="${props.store_url}" \\ + --build-arg VUE_APP_MENU_URL="${props.menu_url}" \\ + --build-arg VUE_APP_SALES_URL="${props.sales_url}" \\ + --build-arg VUE_APP_CONTENT_URL="${props.content_url}" \\ + --build-arg VUE_APP_RECOMMEND_URL="${props.recommend_url}" \\ --build-arg BUILD_FOLDER="deployment/container" \\ --build-arg EXPORT_PORT="${props.export_port}" \\ -f deployment/container/Dockerfile-smarketing-frontend \\ diff --git a/deployment/container/Dockerfile-smarketing-frontend b/deployment/container/Dockerfile-smarketing-frontend index 1f688b7..dd1086a 100644 --- a/deployment/container/Dockerfile-smarketing-frontend +++ b/deployment/container/Dockerfile-smarketing-frontend @@ -1,81 +1,51 @@ # Build stage FROM node:20-slim AS builder + ARG PROJECT_FOLDER +ARG VUE_APP_AUTH_URL +ARG VUE_APP_MEMBER_URL +ARG VUE_APP_STORE_URL +ARG VUE_APP_MENU_URL +ARG VUE_APP_SALES_URL +ARG VUE_APP_CONTENT_URL +ARG VUE_APP_RECOMMEND_URL ENV NODE_ENV=production +ENV VUE_APP_AUTH_URL=${VUE_APP_AUTH_URL} +ENV VUE_APP_MEMBER_URL=${VUE_APP_MEMBER_URL} +ENV VUE_APP_STORE_URL=${VUE_APP_STORE_URL} +ENV VUE_APP_MENU_URL=${VUE_APP_MENU_URL} +ENV VUE_APP_SALES_URL=${VUE_APP_SALES_URL} +ENV VUE_APP_CONTENT_URL=${VUE_APP_CONTENT_URL} +ENV VUE_APP_RECOMMEND_URL=${VUE_APP_RECOMMEND_URL} WORKDIR /app -# Install dependencies +# Copy package files COPY ${PROJECT_FOLDER}/package*.json ./ -RUN npm ci --only=production -# Build application +# Install all dependencies (including devDependencies for build) +RUN npm ci + +# Copy source code COPY ${PROJECT_FOLDER} . + +# Build the application RUN npm run build -# Run stage -FROM nginx:stable-alpine +# Production stage +FROM nginx:alpine AS production -ARG BUILD_FOLDER -ARG EXPORT_PORT -ARG REACT_APP_AUTH_URL -ARG REACT_APP_MEMBER_URL -ARG REACT_APP_STORE_URL -ARG REACT_APP_CONTENT_URL -ARG REACT_APP_RECOMMEND_URL +ARG EXPORT_PORT=18080 -# Create nginx user if it doesn't exist -RUN adduser -S nginx || true +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html -# Copy build files -COPY --from=builder /app/build /usr/share/nginx/html +# Copy nginx configuration +COPY deployment/container/nginx.conf /etc/nginx/conf.d/default.conf -# Create runtime config with all smarketing APIs -# index.html의 헤더에서 이 값을 읽어 환경변수를 생성함 -# api.js에서 이 환경변수를 이용함 -RUN echo "console.log('=== RUNTIME-ENV.JS 로드됨 (Docker 빌드) ==='); \ -window.__runtime_config__ = { \ - AUTH_URL: '${REACT_APP_AUTH_URL}', \ - MEMBER_URL: '${REACT_APP_MEMBER_URL}', \ - STORE_URL: '${REACT_APP_STORE_URL}', \ - CONTENT_URL: '${REACT_APP_CONTENT_URL}', \ - RECOMMEND_URL: '${REACT_APP_RECOMMEND_URL}', \ - ENV: 'production', \ - DEBUG: false, \ - API_TIMEOUT: 30000, \ - RETRY_ATTEMPTS: 3, \ - RETRY_DELAY: 1000, \ - VERSION: '1.0.0', \ - BUILD_DATE: new Date().toISOString() \ -}; \ -window.getApiConfig = () => window.__runtime_config__; \ -window.getApiUrl = (serviceName) => { \ - const config = window.__runtime_config__; \ - const urlKey = \`\${serviceName.toUpperCase()}_URL\`; \ - return config[urlKey] || null; \ -}; \ -console.log('✅ [RUNTIME] Docker 빌드 런타임 설정 로드 완료');" > /usr/share/nginx/html/runtime-env.js - -# Copy and process nginx configuration -COPY ${BUILD_FOLDER}/nginx.conf /etc/nginx/templates/default.conf.template - -# Add custom nginx settings -RUN echo "client_max_body_size 100M;" > /etc/nginx/conf.d/client_max_body_size.conf -RUN echo "proxy_buffer_size 128k;" > /etc/nginx/conf.d/proxy_buffer_size.conf -RUN echo "proxy_buffers 4 256k;" > /etc/nginx/conf.d/proxy_buffers.conf -RUN echo "proxy_busy_buffers_size 256k;" > /etc/nginx/conf.d/proxy_busy_buffers_size.conf - -# Set permissions -RUN chown -R nginx:nginx /usr/share/nginx/html && \ - chmod -R 755 /usr/share/nginx/html && \ - chown -R nginx:nginx /var/cache/nginx && \ - chown -R nginx:nginx /var/log/nginx && \ - chown -R nginx:nginx /etc/nginx/conf.d && \ - touch /var/run/nginx.pid && \ - chown -R nginx:nginx /var/run/nginx.pid - -USER nginx +# Create nginx configuration with custom port +RUN sed -i "s/80/${EXPORT_PORT}/g" /etc/nginx/conf.d/default.conf EXPOSE ${EXPORT_PORT} diff --git a/package.json b/package.json index 4df90f0..5f0ed5e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ai-marketing-frontend", + "name": "smarketing-frontend", "version": "1.0.0", "private": true, "type": "module", @@ -21,6 +21,7 @@ "@vitejs/plugin-vue": "^5.0.0", "vite": "^5.0.0", "vite-plugin-vuetify": "^2.0.0", - "sass": "^1.69.0" + "sass": "^1.69.0", + "eslint": "^8.55.0" } } \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 1727156..fb2a622 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,30 +1,30 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' -import { fileURLToPath, URL } from 'node:url' +import vuetify from 'vite-plugin-vuetify' export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, + plugins: [ + vue(), + vuetify({ + autoImport: true, + }) + ], + build: { + outDir: 'dist', + sourcemap: false, + minify: 'terser' }, server: { port: 3000, - host: true, - open: true, + host: true }, - build: { - outDir: 'dist', - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - vendor: ['vue', 'vue-router', 'pinia'], - vuetify: ['vuetify'], - icons: ['@mdi/font'], - }, - }, - }, - }, -}) + define: { + 'process.env.VUE_APP_AUTH_URL': JSON.stringify(process.env.VUE_APP_AUTH_URL), + 'process.env.VUE_APP_MEMBER_URL': JSON.stringify(process.env.VUE_APP_MEMBER_URL), + 'process.env.VUE_APP_STORE_URL': JSON.stringify(process.env.VUE_APP_STORE_URL), + 'process.env.VUE_APP_MENU_URL': JSON.stringify(process.env.VUE_APP_MENU_URL), + 'process.env.VUE_APP_SALES_URL': JSON.stringify(process.env.VUE_APP_SALES_URL), + 'process.env.VUE_APP_CONTENT_URL': JSON.stringify(process.env.VUE_APP_CONTENT_URL), + 'process.env.VUE_APP_RECOMMEND_URL': JSON.stringify(process.env.VUE_APP_RECOMMEND_URL) + } +}) \ No newline at end of file From d4e0dae167a281709ea030f1932f9716de63982a Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 18:11:50 +0900 Subject: [PATCH 19/36] fix: deployment --- deployment/container/Dockerfile-smarketing-frontend | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/container/Dockerfile-smarketing-frontend b/deployment/container/Dockerfile-smarketing-frontend index dd1086a..de230ae 100644 --- a/deployment/container/Dockerfile-smarketing-frontend +++ b/deployment/container/Dockerfile-smarketing-frontend @@ -10,7 +10,7 @@ ARG VUE_APP_SALES_URL ARG VUE_APP_CONTENT_URL ARG VUE_APP_RECOMMEND_URL -ENV NODE_ENV=production +ENV NODE_ENV=development ENV VUE_APP_AUTH_URL=${VUE_APP_AUTH_URL} ENV VUE_APP_MEMBER_URL=${VUE_APP_MEMBER_URL} ENV VUE_APP_STORE_URL=${VUE_APP_STORE_URL} @@ -24,14 +24,14 @@ WORKDIR /app # Copy package files COPY ${PROJECT_FOLDER}/package*.json ./ -# Install all dependencies (including devDependencies for build) -RUN npm ci +# Install all dependencies (use npm install instead of npm ci) +RUN npm install # Copy source code COPY ${PROJECT_FOLDER} . # Build the application -RUN npm run build +RUN NODE_ENV=production npm run build # Production stage FROM nginx:alpine AS production From f31cc76fbe0ff01800f03190e8886a09ac87cf96 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 18:15:01 +0900 Subject: [PATCH 20/36] fix: deployment --- vite.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vite.config.js b/vite.config.js index fb2a622..b43234e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vuetify from 'vite-plugin-vuetify' +import { fileURLToPath, URL } from 'node:url' export default defineConfig({ plugins: [ @@ -9,6 +10,11 @@ export default defineConfig({ autoImport: true, }) ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, build: { outDir: 'dist', sourcemap: false, From b2906adcdcb15364fc8d051e0f9c73849e5a6788 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Tue, 17 Jun 2025 18:21:01 +0900 Subject: [PATCH 21/36] refactor: remove apiWithImage import --- src/services/menu.js | 374 +++++++------------------------------------ 1 file changed, 56 insertions(+), 318 deletions(-) diff --git a/src/services/menu.js b/src/services/menu.js index 1ecb3e7..294451a 100644 --- a/src/services/menu.js +++ b/src/services/menu.js @@ -1,5 +1,5 @@ -//* src/services/menu.js - 백엔드 수정 없이 프론트엔드만 수정 -import { menuApi, apiWithImage, handleApiError, formatSuccessResponse } from './api.js' +//* src/services/menu.js - apiWithImage 제거하고 menuApi로 통일 +import { menuApi, handleApiError, formatSuccessResponse } from './api.js' /** * 메뉴 관련 API 서비스 @@ -48,7 +48,7 @@ class MenuService { } /** - * 메뉴 등록 (createMenu) + * 메뉴 등록 * @param {Object} menuData - 메뉴 정보 * @returns {Promise} 등록 결과 */ @@ -79,95 +79,9 @@ class MenuService { } } catch (error) { console.error('메뉴 등록 실패:', error) - return handleApiError(error) - } - } - - /** - * 메뉴 등록 (registerMenu 별칭) - * @param {Object} menuData - 메뉴 정보 - * @returns {Promise} 등록 결과 - */ - async registerMenu(menuData) { - return await this.createMenu(menuData) - } - - /** - * 메뉴 수정 - * @param {number} menuId - 메뉴 ID - * @param {Object} menuData - 수정할 메뉴 정보 - * @returns {Promise} 수정 결과 - */ -async updateMenu(menuId, menuData) { - try { - console.log('=== 메뉴 수정 API 호출 ===') - console.log('메뉴 ID:', menuId, '타입:', typeof menuId) - console.log('원본 수정 데이터:', menuData) - - if (!menuId || menuId === 'undefined') { - throw new Error('올바른 메뉴 ID가 필요합니다') - } - - const numericMenuId = parseInt(menuId) - if (isNaN(numericMenuId)) { - throw new Error('메뉴 ID는 숫자여야 합니다') - } - - // 데이터 검증 및 정리 - const menuName = menuData.menuName || menuData.name - const category = menuData.category - const price = menuData.price - const description = menuData.description || '' - - // 필수 필드 검증 - if (!menuName || !category || price === undefined || price === null) { - console.error('필수 필드 누락:', { menuName, category, price, description }) - throw new Error('메뉴명, 카테고리, 가격은 필수 입력 사항입니다') - } - - // 가격 검증 (숫자이고 0 이상) - const numericPrice = parseInt(price) - if (isNaN(numericPrice) || numericPrice < 0) { - throw new Error('올바른 가격을 입력해주세요') - } - - // 백엔드 MenuUpdateRequest DTO에 맞는 데이터 구조 - const requestData = { - menuName: menuName.trim(), - category: category.trim(), - price: numericPrice, - description: description.trim() - } - - console.log('검증된 백엔드 전송 데이터:', requestData) - console.log('✅ 검증된 메뉴 ID:', numericMenuId) - - // PUT /api/menu/{menuId} - const response = await menuApi.put(`/${numericMenuId}`, requestData) - - console.log('메뉴 수정 API 응답:', response.data) - - if (response.data && response.data.status === 200) { - return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 수정되었습니다.') - } else { - throw new Error(response.data.message || '메뉴 수정에 실패했습니다.') - } - } catch (error) { - console.error('메뉴 수정 실패:', error) - - // HTTP 응답 에러 상세 디버깅 - if (error.response) { - console.error('=== HTTP 응답 에러 상세 ===') - console.error('상태 코드:', error.response.status) - console.error('상태 텍스트:', error.response.statusText) - console.error('응답 데이터:', error.response.data) - console.error('요청 URL:', error.config?.url) - console.error('요청 메서드:', error.config?.method) - console.error('요청 데이터:', error.config?.data) - // 400 에러 (잘못된 요청) 처리 - if (error.response.status === 400) { - const errorMessage = error.response.data?.message || '입력 데이터가 올바르지 않습니다.' + if (error.response?.status === 400) { + const errorMessage = error.response.data?.message || 'validation 에러가 발생했습니다.' console.error('백엔드 validation 에러:', errorMessage) return { @@ -177,8 +91,7 @@ async updateMenu(menuId, menuData) { } } - // 500 오류 처리 - if (error.response.status === 500) { + if (error.response?.status === 500) { const errorMessage = error.response.data?.message || '서버 내부 오류가 발생했습니다.' console.error('백엔드 에러 메시지:', errorMessage) @@ -188,15 +101,58 @@ async updateMenu(menuId, menuData) { error: error.response.data } } + + return handleApiError(error) } - - return handleApiError(error) } -} - /** - * 메뉴 이미지 업로드 + * 메뉴 수정 + * @param {number} menuId - 메뉴 ID + * @param {Object} menuData - 수정할 메뉴 정보 + * @returns {Promise} 수정 결과 + */ + async updateMenu(menuId, menuData) { + try { + console.log('=== 메뉴 수정 API 호출 ===') + console.log('메뉴 ID:', menuId, '수정 데이터:', menuData) + + if (!menuId || menuId === 'undefined') { + throw new Error('올바른 메뉴 ID가 필요합니다') + } + + const numericMenuId = parseInt(menuId) + if (isNaN(numericMenuId)) { + throw new Error('메뉴 ID는 숫자여야 합니다') + } + + const requestData = { + menuName: menuData.menuName || menuData.name, + category: menuData.category, + price: parseInt(menuData.price) || 0, + description: menuData.description || '' + } + + console.log('백엔드 전송 데이터:', requestData) + + // PUT /api/menu/{menuId} + const response = await menuApi.put(`/${numericMenuId}`, requestData) + + console.log('메뉴 수정 API 응답:', response.data) + + if (response.data && response.data.status === 200) { + return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 수정되었습니다.') + } else { + throw new Error(response.data.message || '메뉴 수정에 실패했습니다.') + } + } catch (error) { + console.error('메뉴 수정 실패:', error) + return handleApiError(error) + } + } + + /** + * 메뉴 이미지 업로드 (menuApi 사용) * @param {number} menuId - 메뉴 ID * @param {File} file - 이미지 파일 * @returns {Promise} 업로드 결과 @@ -225,8 +181,8 @@ async updateMenu(menuId, menuData) { console.log('이미지 업로드 요청 - 메뉴 ID:', numericMenuId) - // POST /api/images/menu/{menuId} - const response = await apiWithImage.post(`/images/menu/${numericMenuId}`, formData, { + // POST /api/menu/images/{menuId} (menuApi 사용) + const response = await menuApi.post(`/images/${numericMenuId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } @@ -288,222 +244,4 @@ export default menuService // 디버깅을 위한 전역 노출 (개발 환경에서만) if (process.env.NODE_ENV === 'development') { window.menuService = menuService -} - -//* src/views/StoreManagementView.vue의 수정된 스크립트 부분 -// + AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션
- - - From e6f2c3a810022d01f7b5588b2a9a33bbcecf0470 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 13:48:10 +0900 Subject: [PATCH 26/36] refactor: all --- deployment/container/nginx.conf | 126 +++++++++++++++++--- deployment/deploy.yaml.template | 3 +- deployment/deploy_env_vars | 47 ++++++-- deployment/manifest/deployment.yaml | 39 +++++- deployment/manifest/frontend-configmap.yaml | 4 +- public/runtime-env.js | 97 +++++++++++---- 6 files changed, 266 insertions(+), 50 deletions(-) diff --git a/deployment/container/nginx.conf b/deployment/container/nginx.conf index 685b08c..8bbf9a5 100644 --- a/deployment/container/nginx.conf +++ b/deployment/container/nginx.conf @@ -4,7 +4,15 @@ server { root /usr/share/nginx/html; index index.html index.htm; - # Gzip compression + # 에러 페이지 설정 + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + + # 로깅 설정 + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + # Gzip compression 최적화 gzip on; gzip_vary on; gzip_min_length 1024; @@ -17,30 +25,118 @@ server { text/javascript application/javascript application/xml+rss - application/json; + application/json + application/xml + image/svg+xml; - # Handle client routing (Vue Router) - location / { - try_files $uri $uri/ /index.html; - } + # Brotli compression (더 좋은 압축률) + # brotli on; + # brotli_comp_level 6; + # brotli_types text/xml image/svg+xml application/x-font-ttf image/vnd.microsoft.icon application/x-font-opentype application/json font/eot application/vnd.ms-fontobject application/javascript font/otf application/xml application/xhtml+xml text/javascript application/x-javascript text/plain application/x-font-truetype application/xml+rss image/x-icon font/opentype text/css image/x-win-bitmap; - # Security headers + # 보안 헤더 강화 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; - add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' http://smarketing.20.249.184.228.nip.io https://smarketing.20.249.184.228.nip.io" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { - expires 1y; - add_header Cache-Control "public, immutable"; + # CORS 설정 (필요시) + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; + + # SPA 라우팅 처리 (Vue Router) + location / { + try_files $uri $uri/ /index.html; + + # HTML 파일은 캐시하지 않음 (런타임 설정 반영 위해) + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } } - # Health check endpoint + # 런타임 환경 설정 파일 - 캐시하지 않음 + location /runtime-env.js { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } + + # 정적 자산 캐시 최적화 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + + # 정적 파일에 대한 로깅 최소화 + access_log off; + } + + # manifest.json과 service worker 캐시 설정 + location ~* \.(json|webmanifest)$ { + expires 1d; + add_header Cache-Control "public"; + } + + location /sw.js { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # 파비콘 캐시 + location /favicon.ico { + expires 1M; + access_log off; + log_not_found off; + } + + # 헬스체크 엔드포인트 location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } -} \ No newline at end of file + + # API 프록시 (필요시 백엔드로 직접 프록시) + # location /api/ { + # proxy_pass http://smarketing.20.249.184.228.nip.io/api/; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_connect_timeout 30s; + # proxy_send_timeout 30s; + # proxy_read_timeout 30s; + # } + + # 숨겨진 파일 접근 차단 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # 불필요한 파일 접근 차단 + location ~* \.(log|txt|md|yml|yaml|conf)$ { + deny all; + access_log off; + log_not_found off; + } + + # 압축된 자산 우선 제공 + location ~* \.(js|css)$ { + gzip_static on; + expires 1y; + add_header Cache-Control "public, immutable"; + } +} + +# 추가 서버 블록 (HTTPS 리다이렉트, 필요시) +# server { +# listen 8080; +# server_name smarketing.20.249.184.228.nip.io; +# return 301 https://$server_name$request_uri; +# } \ No newline at end of file diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template index 5379137..63a8d68 100644 --- a/deployment/deploy.yaml.template +++ b/deployment/deploy.yaml.template @@ -140,7 +140,7 @@ metadata: labels: app: smarketing-frontend spec: - type: LoadBalancer + type: ClusterIP ports: - port: 80 targetPort: ${export_port} @@ -157,6 +157,7 @@ metadata: namespace: ${namespace} annotations: nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" spec: rules: - host: ${ingress_host} diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars index aa706ef..e449517 100644 --- a/deployment/deploy_env_vars +++ b/deployment/deploy_env_vars @@ -14,14 +14,14 @@ export_port=18080 # Gateway/Ingress 설정 (⭐ smarketing-backend와 동일한 IP 사용) ingress_host=smarketing.20.249.184.228.nip.io -# 리소스 설정 -resources_requests_cpu=256m -resources_requests_memory=256Mi -resources_limits_cpu=1024m -resources_limits_memory=1024Mi +# 리소스 설정 (프론트엔드에 맞게 조정) +resources_requests_cpu=128m # 프론트엔드는 CPU 사용량이 적음 +resources_requests_memory=128Mi # 메모리도 적게 사용 +resources_limits_cpu=512m # 제한도 낮게 설정 +resources_limits_memory=512Mi # API URLs (⭐ smarketing-backend ingress를 통해 라우팅) -# 현재 설정된 백엔드 API들과 일치 +# 백엔드 서비스별 API 경로들 auth_url=http://smarketing.20.249.184.228.nip.io/api/auth member_url=http://smarketing.20.249.184.228.nip.io/api/member store_url=http://smarketing.20.249.184.228.nip.io/api/store @@ -30,6 +30,39 @@ sales_url=http://smarketing.20.249.184.228.nip.io/api/sales content_url=http://smarketing.20.249.184.228.nip.io/api/content recommend_url=http://smarketing.20.249.184.228.nip.io/api/recommend +# Frontend 이미지 경로 설정 +smarketing_frontend_image_path=${registry}/${image_org}/smarketing-frontend:latest + # GitHub 설정 github_org=won-ktds -teamid=smarketing \ No newline at end of file +teamid=smarketing + +# 환경 플래그 +environment=production +debug_mode=false + +# SSL/TLS 설정 (필요시) +ssl_enabled=false +ssl_redirect=false + +# 로깅 레벨 +log_level=info + +# 헬스체크 설정 +health_check_path=/health +health_check_interval=30s +health_check_timeout=5s +health_check_retries=3 + +# 보안 설정 +security_headers_enabled=true +cors_enabled=true +allowed_origins=* + +# 캐시 설정 +static_cache_enabled=true +static_cache_duration=1y + +# 압축 설정 +gzip_enabled=true +gzip_compression_level=6 \ No newline at end of file diff --git a/deployment/manifest/deployment.yaml b/deployment/manifest/deployment.yaml index 272d253..6d19513 100644 --- a/deployment/manifest/deployment.yaml +++ b/deployment/manifest/deployment.yaml @@ -5,6 +5,7 @@ metadata: namespace: ${namespace} labels: app: smarketing-frontend + version: v1 spec: replicas: ${replicas} selector: @@ -14,6 +15,7 @@ spec: metadata: labels: app: smarketing-frontend + version: v1 spec: imagePullSecrets: - name: acr-secret @@ -23,6 +25,7 @@ spec: imagePullPolicy: Always ports: - containerPort: ${export_port} + name: http resources: requests: cpu: ${resources_requests_cpu} @@ -34,19 +37,53 @@ spec: - name: runtime-config mountPath: /usr/share/nginx/html/runtime-env.js subPath: runtime-env.js + readOnly: true + env: + - name: NGINX_PORT + value: "${export_port}" livenessProbe: httpGet: path: /health port: ${export_port} + scheme: HTTP initialDelaySeconds: 30 periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 1 readinessProbe: httpGet: path: /health port: ${export_port} + scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + successThreshold: 1 + # 시작 프로브 추가 (컨테이너 시작 시간이 오래 걸릴 수 있음) + startupProbe: + httpGet: + path: /health + port: ${export_port} + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + successThreshold: 1 volumes: - name: runtime-config configMap: - name: smarketing-frontend-config \ No newline at end of file + name: smarketing-frontend-config + defaultMode: 0644 + # 배포 전략 설정 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + # 재시작 정책 + restartPolicy: Always + # DNS 정책 + dnsPolicy: ClusterFirst \ No newline at end of file diff --git a/deployment/manifest/frontend-configmap.yaml b/deployment/manifest/frontend-configmap.yaml index d111f6e..e3e164d 100644 --- a/deployment/manifest/frontend-configmap.yaml +++ b/deployment/manifest/frontend-configmap.yaml @@ -8,14 +8,14 @@ data: console.log('=== RUNTIME-ENV.JS 로드됨 (배포 환경) ==='); window.__runtime_config__ = { - // 백엔드 API 구조에 맞게 URL 설정 + // 백엔드 API 구조에 맞게 URL 설정 - ingress host 사용 AUTH_URL: 'http://${ingress_host}/api/auth', MEMBER_URL: 'http://${ingress_host}/api/member', STORE_URL: 'http://${ingress_host}/api/store', MENU_URL: 'http://${ingress_host}/api/menu', SALES_URL: 'http://${ingress_host}/api/sales', CONTENT_URL: 'http://${ingress_host}/api/content', - RECOMMEND_URL: 'http://${ingress_host}/api/recommendations', + RECOMMEND_URL: 'http://${ingress_host}/api/recommend', // Gateway URL (운영 환경) GATEWAY_URL: 'http://${ingress_host}', diff --git a/public/runtime-env.js b/public/runtime-env.js index 0dff287..8673a31 100644 --- a/public/runtime-env.js +++ b/public/runtime-env.js @@ -1,18 +1,62 @@ -//* public/runtime-env.js - 백엔드 API 경로에 맞게 수정 +//* public/runtime-env.js - 배포 환경 우선 설정 console.log('=== RUNTIME-ENV.JS 로드됨 ==='); +// 배포 환경 감지 함수 +const isProduction = () => { + // 프로덕션 환경 감지 로직 + return window.location.hostname !== 'localhost' && + window.location.hostname !== '127.0.0.1' && + !window.location.hostname.includes('dev'); +}; + +// 기본 ingress host 설정 (deploy_env_vars에서 설정된 값) +const DEFAULT_INGRESS_HOST = 'smarketing.20.249.184.228.nip.io'; + +// 환경별 API URL 설정 +const getBaseUrl = () => { + if (isProduction()) { + // 프로덕션: ingress host 사용 + return `http://${DEFAULT_INGRESS_HOST}`; + } else { + // 개발환경: localhost 사용 + return ''; + } +}; + +const baseUrl = getBaseUrl(); + window.__runtime_config__ = { - // ⚠️ 수정: 백엔드 API 구조에 맞게 URL 설정 - AUTH_URL: 'http://localhost:8081/api/auth', - MEMBER_URL: 'http://localhost:8081/api/member', - STORE_URL: 'http://localhost:8082/api/store', - MENU_URL: 'http://localhost:8082/api/menu', - SALES_URL: 'http://localhost:8082/api/sales', // store 서비스 - CONTENT_URL: 'http://localhost:8083/api/content', - RECOMMEND_URL: 'http://localhost:8084/api/recommendations', // ⚠️ 수정: 올바른 경로 + // 프로덕션 환경에서는 ingress host 사용, 개발환경에서는 localhost + AUTH_URL: isProduction() ? + `${baseUrl}/api/auth` : + 'http://localhost:8081/api/auth', + + MEMBER_URL: isProduction() ? + `${baseUrl}/api/member` : + 'http://localhost:8081/api/member', + + STORE_URL: isProduction() ? + `${baseUrl}/api/store` : + 'http://localhost:8082/api/store', + + MENU_URL: isProduction() ? + `${baseUrl}/api/menu` : + 'http://localhost:8082/api/menu', + + SALES_URL: isProduction() ? + `${baseUrl}/api/sales` : + 'http://localhost:8082/api/sales', + + CONTENT_URL: isProduction() ? + `${baseUrl}/api/content` : + 'http://localhost:8083/api/content', + + RECOMMEND_URL: isProduction() ? + `${baseUrl}/api/recommend` : + 'http://localhost:8084/api/recommendations', - // Gateway URL (운영 환경용) - GATEWAY_URL: 'http://20.1.2.3', + // Gateway URL + GATEWAY_URL: isProduction() ? baseUrl : 'http://20.1.2.3', // 기능 플래그 FEATURES: { @@ -20,17 +64,17 @@ window.__runtime_config__ = { PUSH_NOTIFICATIONS: true, SOCIAL_LOGIN: false, MULTI_LANGUAGE: false, - API_HEALTH_CHECK: true, // ⚠️ 추가 + API_HEALTH_CHECK: true, }, // 환경 설정 - ENV: 'development', - DEBUG: true, + ENV: isProduction() ? 'production' : 'development', + DEBUG: !isProduction(), - // ⚠️ 추가: API 타임아웃 설정 + // API 타임아웃 설정 API_TIMEOUT: 30000, - // ⚠️ 추가: 재시도 설정 + // 재시도 설정 RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000, @@ -38,7 +82,7 @@ window.__runtime_config__ = { VERSION: '1.0.0', BUILD_DATE: new Date().toISOString(), - // ⚠️ 추가: 백엔드 서비스 포트 정보 (디버깅용) + // 백엔드 서비스 포트 정보 (디버깅용) BACKEND_PORTS: { AUTH: 8081, STORE: 8082, @@ -47,7 +91,7 @@ window.__runtime_config__ = { } }; -// ⚠️ 추가: 설정 검증 함수 +// 설정 검증 함수 const validateConfig = () => { const config = window.__runtime_config__; const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL']; @@ -63,8 +107,13 @@ const validateConfig = () => { return true; }; -// ⚠️ 추가: 개발 환경에서만 상세 로깅 +// 환경별 상세 로깅 if (window.__runtime_config__.DEBUG) { + console.log('=== 현재 환경 정보 ==='); + console.log('🌍 Environment:', window.__runtime_config__.ENV); + console.log('🏠 Hostname:', window.location.hostname); + console.log('🔧 Is Production:', isProduction()); + console.log('=== 백엔드 API URLs ==='); console.log('🔐 AUTH_URL:', window.__runtime_config__.AUTH_URL); console.log('🏪 STORE_URL:', window.__runtime_config__.STORE_URL); @@ -74,12 +123,9 @@ if (window.__runtime_config__.DEBUG) { console.log('=== 설정 상세 정보 ==='); console.log('전체 설정:', window.__runtime_config__); - - // 설정 검증 실행 - validateConfig(); } -// ⚠️ 추가: 전역 설정 접근 함수 +// 전역 설정 접근 함수 window.getApiConfig = () => window.__runtime_config__; window.getApiUrl = (serviceName) => { const config = window.__runtime_config__; @@ -87,4 +133,7 @@ window.getApiUrl = (serviceName) => { return config[urlKey] || null; }; -console.log('✅ [RUNTIME] 런타임 설정 로드 완료'); \ No newline at end of file +// 설정 검증 실행 +validateConfig(); + +console.log(`✅ [RUNTIME] 런타임 설정 로드 완료 (${window.__runtime_config__.ENV} 환경)`); \ No newline at end of file From e23857a6e2e21fc116a3b1fde25106fc282c53a6 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 13:55:18 +0900 Subject: [PATCH 27/36] refactor: ClusterIP to LoadBalancer --- deployment/manifest/service.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/manifest/service.yaml b/deployment/manifest/service.yaml index fcb845a..f6c623f 100644 --- a/deployment/manifest/service.yaml +++ b/deployment/manifest/service.yaml @@ -6,7 +6,7 @@ metadata: labels: app: smarketing-frontend spec: - type: ClusterIP + type: LoadBalancer ports: - port: 80 targetPort: ${export_port} From abc52f01400ba18a9c71279c377c00ceb4ce2706 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 14:07:40 +0900 Subject: [PATCH 28/36] refactor: ClusterIP to LoadBalancer --- deployment/deploy.yaml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template index 63a8d68..a7ce0a2 100644 --- a/deployment/deploy.yaml.template +++ b/deployment/deploy.yaml.template @@ -140,7 +140,7 @@ metadata: labels: app: smarketing-frontend spec: - type: ClusterIP + type: LoadBalancer ports: - port: 80 targetPort: ${export_port} From 6be48d503d597447432b7b91ad6a62821d4a1b2c Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 14:18:51 +0900 Subject: [PATCH 29/36] refactor: Dockerfile --- deployment/container/Dockerfile-smarketing-frontend | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/deployment/container/Dockerfile-smarketing-frontend b/deployment/container/Dockerfile-smarketing-frontend index 542d749..d2c8ede 100644 --- a/deployment/container/Dockerfile-smarketing-frontend +++ b/deployment/container/Dockerfile-smarketing-frontend @@ -24,15 +24,21 @@ WORKDIR /app # Copy package files COPY ${PROJECT_FOLDER}/package*.json ./ -# Install all dependencies (use npm install instead of npm ci) +# Install all dependencies RUN npm install # Copy source code COPY ${PROJECT_FOLDER} . +# ⚠️ 중요: public/runtime-env.js 삭제 (ConfigMap으로 대체) +RUN rm -f public/runtime-env.js + # Build the application RUN NODE_ENV=production npm run build +# ⚠️ 빌드된 파일에서도 runtime-env.js 삭제 +RUN rm -f dist/runtime-env.js + # Production stage FROM nginx:alpine AS production @@ -41,9 +47,12 @@ ARG EXPORT_PORT=18080 # Copy built files from builder stage COPY --from=builder /app/dist /usr/share/nginx/html -# Copy nginx configuration (이미 포트가 18080으로 설정됨) +# Copy nginx configuration COPY deployment/container/nginx.conf /etc/nginx/conf.d/default.conf +# ⚠️ 더미 runtime-env.js 생성 (ConfigMap으로 덮어씌워질 예정) +RUN echo "console.log('Runtime config will be loaded from ConfigMap');" > /usr/share/nginx/html/runtime-env.js + EXPOSE ${EXPORT_PORT} CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From e544645d7c0a4551dafb9fcb2ad5d80bb3f4e2bd Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 14:31:20 +0900 Subject: [PATCH 30/36] refactor: nginx --- deployment/container/nginx.conf | 141 +++++++++++--------------------- 1 file changed, 48 insertions(+), 93 deletions(-) diff --git a/deployment/container/nginx.conf b/deployment/container/nginx.conf index 8bbf9a5..65b4a14 100644 --- a/deployment/container/nginx.conf +++ b/deployment/container/nginx.conf @@ -8,11 +8,49 @@ server { error_page 404 /index.html; error_page 500 502 503 504 /50x.html; - # 로깅 설정 + # 로깅 설정 (디버깅용) access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log warn; + error_log /var/log/nginx/error.log debug; - # Gzip compression 최적화 + # ⚠️ 중요: runtime-env.js 파일 특별 처리 + location = /runtime-env.js { + try_files $uri $uri/ =404; + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Content-Type "application/javascript"; + + # CORS 헤더 추가 + add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Methods "GET, OPTIONS"; + add_header Access-Control-Allow-Headers "Content-Type"; + } + + # SPA 라우팅 처리 (Vue Router) + location / { + try_files $uri $uri/ /index.html; + + # HTML 파일은 캐시하지 않음 + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } + } + + # 정적 자산 캐시 최적화 (runtime-env.js 제외) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + # runtime-env.js는 위에서 별도 처리되므로 제외 + if ($uri = "/runtime-env.js") { + break; + } + + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; @@ -29,61 +67,18 @@ server { application/xml image/svg+xml; - # Brotli compression (더 좋은 압축률) - # brotli on; - # brotli_comp_level 6; - # brotli_types text/xml image/svg+xml application/x-font-ttf image/vnd.microsoft.icon application/x-font-opentype application/json font/eot application/vnd.ms-fontobject application/javascript font/otf application/xml application/xhtml+xml text/javascript application/x-javascript text/plain application/x-font-truetype application/xml+rss image/x-icon font/opentype text/css image/x-win-bitmap; - - # 보안 헤더 강화 + # 보안 헤더 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' http://smarketing.20.249.184.228.nip.io https://smarketing.20.249.184.228.nip.io" always; - add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http://smarketing.20.249.184.228.nip.io https://smarketing.20.249.184.228.nip.io" always; - # CORS 설정 (필요시) - add_header Access-Control-Allow-Origin "*" always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; - - # SPA 라우팅 처리 (Vue Router) - location / { - try_files $uri $uri/ /index.html; - - # HTML 파일은 캐시하지 않음 (런타임 설정 반영 위해) - location ~* \.html$ { - expires -1; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - } - } - - # 런타임 환경 설정 파일 - 캐시하지 않음 - location /runtime-env.js { - expires -1; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - } - - # 정적 자산 캐시 최적화 - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - - # 정적 파일에 대한 로깅 최소화 + # 헬스체크 엔드포인트 + location /health { access_log off; - } - - # manifest.json과 service worker 캐시 설정 - location ~* \.(json|webmanifest)$ { - expires 1d; - add_header Cache-Control "public"; - } - - location /sw.js { - expires -1; - add_header Cache-Control "no-cache, no-store, must-revalidate"; + return 200 "healthy\n"; + add_header Content-Type text/plain; } # 파비콘 캐시 @@ -93,50 +88,10 @@ server { log_not_found off; } - # 헬스체크 엔드포인트 - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } - - # API 프록시 (필요시 백엔드로 직접 프록시) - # location /api/ { - # proxy_pass http://smarketing.20.249.184.228.nip.io/api/; - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # proxy_set_header X-Forwarded-Proto $scheme; - # proxy_connect_timeout 30s; - # proxy_send_timeout 30s; - # proxy_read_timeout 30s; - # } - # 숨겨진 파일 접근 차단 location ~ /\. { deny all; access_log off; log_not_found off; } - - # 불필요한 파일 접근 차단 - location ~* \.(log|txt|md|yml|yaml|conf)$ { - deny all; - access_log off; - log_not_found off; - } - - # 압축된 자산 우선 제공 - location ~* \.(js|css)$ { - gzip_static on; - expires 1y; - add_header Cache-Control "public, immutable"; - } -} - -# 추가 서버 블록 (HTTPS 리다이렉트, 필요시) -# server { -# listen 8080; -# server_name smarketing.20.249.184.228.nip.io; -# return 301 https://$server_name$request_uri; -# } \ No newline at end of file +} \ No newline at end of file From 44275a91e65262c8a73337afeb13fb024991367d Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 14:52:48 +0900 Subject: [PATCH 31/36] refactor: remove korean in configmap --- deployment/manifest/frontend-configmap.yaml | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/deployment/manifest/frontend-configmap.yaml b/deployment/manifest/frontend-configmap.yaml index e3e164d..b42eff2 100644 --- a/deployment/manifest/frontend-configmap.yaml +++ b/deployment/manifest/frontend-configmap.yaml @@ -5,10 +5,10 @@ metadata: namespace: ${namespace} data: runtime-env.js: | - console.log('=== RUNTIME-ENV.JS 로드됨 (배포 환경) ==='); + console.log('=== RUNTIME-ENV.JS LOADED (PRODUCTION) ==='); window.__runtime_config__ = { - // 백엔드 API 구조에 맞게 URL 설정 - ingress host 사용 + // Backend API URLs using ingress host AUTH_URL: 'http://${ingress_host}/api/auth', MEMBER_URL: 'http://${ingress_host}/api/member', STORE_URL: 'http://${ingress_host}/api/store', @@ -17,10 +17,10 @@ data: CONTENT_URL: 'http://${ingress_host}/api/content', RECOMMEND_URL: 'http://${ingress_host}/api/recommend', - // Gateway URL (운영 환경) + // Gateway URL (production environment) GATEWAY_URL: 'http://${ingress_host}', - // 기능 플래그 + // Feature flags FEATURES: { ANALYTICS: true, PUSH_NOTIFICATIONS: true, @@ -29,46 +29,46 @@ data: API_HEALTH_CHECK: true, }, - // 환경 설정 (배포 환경) + // Environment settings (production) ENV: 'production', DEBUG: false, - // API 타임아웃 설정 + // API timeout settings API_TIMEOUT: 30000, - // 재시도 설정 + // Retry settings RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000, - // 버전 정보 + // Version info VERSION: '1.0.0', BUILD_DATE: new Date().toISOString() }; - // 설정 검증 함수 + // Config validation function const validateConfig = () => { const config = window.__runtime_config__; const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL']; for (const url of requiredUrls) { if (!config[url]) { - console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`); + console.error('Missing required URL: ' + url); return false; } } - console.log('✅ [CONFIG] 설정 검증 완료'); + console.log('Config validation completed'); return true; }; - // 전역 설정 접근 함수 + // Global config access functions window.getApiConfig = () => window.__runtime_config__; window.getApiUrl = (serviceName) => { const config = window.__runtime_config__; - const urlKey = `${serviceName.toUpperCase()}_URL`; + const urlKey = serviceName.toUpperCase() + '_URL'; return config[urlKey] || null; }; - // 설정 검증 실행 + // Execute validation validateConfig(); - console.log('✅ [RUNTIME] 런타임 설정 로드 완료 (배포 환경)'); \ No newline at end of file + console.log('Runtime config loaded successfully (PRODUCTION)'); \ No newline at end of file From bc72e45408135d1357b2be2910b0abd289dd3428 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 15:02:07 +0900 Subject: [PATCH 32/36] refactor: api.js hard coding --- src/services/api.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/services/api.js b/src/services/api.js index 6fbd09d..496ae7c 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -6,15 +6,15 @@ const getApiUrls = () => { const config = window.__runtime_config__ || {} return { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', - AUTH_URL: config.AUTH_URL || 'http://localhost:8081/api/auth', - MEMBER_URL: config.MEMBER_URL || 'http://localhost:8081/api/member', - STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', - CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content', - MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', + AUTH_URL: config.AUTH_URL || 'http://smarketing.20.249.184.228.nip.io/api/auth', + MEMBER_URL: config.MEMBER_URL || 'http://smarketing.20.249.184.228.nip.io/api/member', + STORE_URL: config.STORE_URL || 'http://smarketing.20.249.184.228.nip.io/api/store', + CONTENT_URL: config.CONTENT_URL || 'http://smarketing.20.249.184.228.nip.io/api/content', + MENU_URL: config.MENU_URL || 'http://smarketing.20.249.184.228.nip.io/api/menu', // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) - SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', + SALES_URL: config.SALES_URL || 'http://smarketing.20.249.184.228.nip.io/api/sales', // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) - RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations' + RECOMMEND_URL: config.RECOMMEND_URL || 'http://smarketing.20.249.184.228.nip.io/api/recommendations' } } From 142a8206f36bbd143187dcbdcd1c353e024f8808 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 15:17:06 +0900 Subject: [PATCH 33/36] refactor: runtime-env remove korean --- public/runtime-env.js | 68 +++++++++++++++++++++---------------------- src/services/api.js | 14 ++++----- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/public/runtime-env.js b/public/runtime-env.js index 8673a31..61dad8f 100644 --- a/public/runtime-env.js +++ b/public/runtime-env.js @@ -1,24 +1,24 @@ -//* public/runtime-env.js - 배포 환경 우선 설정 -console.log('=== RUNTIME-ENV.JS 로드됨 ==='); +//* public/runtime-env.js - Production environment priority configuration +console.log('=== RUNTIME-ENV.JS LOADED ==='); -// 배포 환경 감지 함수 +// Production environment detection function const isProduction = () => { - // 프로덕션 환경 감지 로직 + // Production environment detection logic return window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && !window.location.hostname.includes('dev'); }; -// 기본 ingress host 설정 (deploy_env_vars에서 설정된 값) +// Default ingress host setting (from deploy_env_vars) const DEFAULT_INGRESS_HOST = 'smarketing.20.249.184.228.nip.io'; -// 환경별 API URL 설정 +// Environment-specific API URL configuration const getBaseUrl = () => { if (isProduction()) { - // 프로덕션: ingress host 사용 + // Production: use ingress host return `http://${DEFAULT_INGRESS_HOST}`; } else { - // 개발환경: localhost 사용 + // Development: use localhost return ''; } }; @@ -26,7 +26,7 @@ const getBaseUrl = () => { const baseUrl = getBaseUrl(); window.__runtime_config__ = { - // 프로덕션 환경에서는 ingress host 사용, 개발환경에서는 localhost + // Use ingress host in production, localhost in development AUTH_URL: isProduction() ? `${baseUrl}/api/auth` : 'http://localhost:8081/api/auth', @@ -58,7 +58,7 @@ window.__runtime_config__ = { // Gateway URL GATEWAY_URL: isProduction() ? baseUrl : 'http://20.1.2.3', - // 기능 플래그 + // Feature flags FEATURES: { ANALYTICS: true, PUSH_NOTIFICATIONS: true, @@ -67,22 +67,22 @@ window.__runtime_config__ = { API_HEALTH_CHECK: true, }, - // 환경 설정 + // Environment settings ENV: isProduction() ? 'production' : 'development', DEBUG: !isProduction(), - // API 타임아웃 설정 + // API timeout settings API_TIMEOUT: 30000, - // 재시도 설정 + // Retry settings RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000, - // 버전 정보 + // Version information VERSION: '1.0.0', BUILD_DATE: new Date().toISOString(), - // 백엔드 서비스 포트 정보 (디버깅용) + // Backend service port information (for debugging) BACKEND_PORTS: { AUTH: 8081, STORE: 8082, @@ -91,41 +91,41 @@ window.__runtime_config__ = { } }; -// 설정 검증 함수 +// Configuration validation function const validateConfig = () => { const config = window.__runtime_config__; const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL']; for (const url of requiredUrls) { if (!config[url]) { - console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`); + console.error(`Missing required URL: ${url}`); return false; } } - console.log('✅ [CONFIG] 설정 검증 완료'); + console.log('Config validation completed'); return true; }; -// 환경별 상세 로깅 +// Environment-specific detailed logging if (window.__runtime_config__.DEBUG) { - console.log('=== 현재 환경 정보 ==='); - console.log('🌍 Environment:', window.__runtime_config__.ENV); - console.log('🏠 Hostname:', window.location.hostname); - console.log('🔧 Is Production:', isProduction()); + console.log('=== Current Environment Info ==='); + console.log('Environment:', window.__runtime_config__.ENV); + console.log('Hostname:', window.location.hostname); + console.log('Is Production:', isProduction()); - console.log('=== 백엔드 API URLs ==='); - console.log('🔐 AUTH_URL:', window.__runtime_config__.AUTH_URL); - console.log('🏪 STORE_URL:', window.__runtime_config__.STORE_URL); - console.log('📊 SALES_URL:', window.__runtime_config__.SALES_URL); - console.log('🤖 RECOMMEND_URL:', window.__runtime_config__.RECOMMEND_URL); - console.log('📄 CONTENT_URL:', window.__runtime_config__.CONTENT_URL); + console.log('=== Backend API URLs ==='); + console.log('AUTH_URL:', window.__runtime_config__.AUTH_URL); + console.log('STORE_URL:', window.__runtime_config__.STORE_URL); + console.log('SALES_URL:', window.__runtime_config__.SALES_URL); + console.log('RECOMMEND_URL:', window.__runtime_config__.RECOMMEND_URL); + console.log('CONTENT_URL:', window.__runtime_config__.CONTENT_URL); - console.log('=== 설정 상세 정보 ==='); - console.log('전체 설정:', window.__runtime_config__); + console.log('=== Detailed Configuration ==='); + console.log('Full config:', window.__runtime_config__); } -// 전역 설정 접근 함수 +// Global configuration access functions window.getApiConfig = () => window.__runtime_config__; window.getApiUrl = (serviceName) => { const config = window.__runtime_config__; @@ -133,7 +133,7 @@ window.getApiUrl = (serviceName) => { return config[urlKey] || null; }; -// 설정 검증 실행 +// Execute configuration validation validateConfig(); -console.log(`✅ [RUNTIME] 런타임 설정 로드 완료 (${window.__runtime_config__.ENV} 환경)`); \ No newline at end of file +console.log(`Runtime configuration loaded successfully (${window.__runtime_config__.ENV} environment)`); \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index 496ae7c..6fbd09d 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -6,15 +6,15 @@ const getApiUrls = () => { const config = window.__runtime_config__ || {} return { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', - AUTH_URL: config.AUTH_URL || 'http://smarketing.20.249.184.228.nip.io/api/auth', - MEMBER_URL: config.MEMBER_URL || 'http://smarketing.20.249.184.228.nip.io/api/member', - STORE_URL: config.STORE_URL || 'http://smarketing.20.249.184.228.nip.io/api/store', - CONTENT_URL: config.CONTENT_URL || 'http://smarketing.20.249.184.228.nip.io/api/content', - MENU_URL: config.MENU_URL || 'http://smarketing.20.249.184.228.nip.io/api/menu', + AUTH_URL: config.AUTH_URL || 'http://localhost:8081/api/auth', + MEMBER_URL: config.MEMBER_URL || 'http://localhost:8081/api/member', + STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', + CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content', + MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) - SALES_URL: config.SALES_URL || 'http://smarketing.20.249.184.228.nip.io/api/sales', + SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) - RECOMMEND_URL: config.RECOMMEND_URL || 'http://smarketing.20.249.184.228.nip.io/api/recommendations' + RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations' } } From 422e590e2bc0e9dfb3cdfa43098948638967e13a Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 15:42:36 +0900 Subject: [PATCH 34/36] refactor: api.js --- src/services/api.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/services/api.js b/src/services/api.js index 6fbd09d..496ae7c 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -6,15 +6,15 @@ const getApiUrls = () => { const config = window.__runtime_config__ || {} return { GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3', - AUTH_URL: config.AUTH_URL || 'http://localhost:8081/api/auth', - MEMBER_URL: config.MEMBER_URL || 'http://localhost:8081/api/member', - STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', - CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content', - MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', + AUTH_URL: config.AUTH_URL || 'http://smarketing.20.249.184.228.nip.io/api/auth', + MEMBER_URL: config.MEMBER_URL || 'http://smarketing.20.249.184.228.nip.io/api/member', + STORE_URL: config.STORE_URL || 'http://smarketing.20.249.184.228.nip.io/api/store', + CONTENT_URL: config.CONTENT_URL || 'http://smarketing.20.249.184.228.nip.io/api/content', + MENU_URL: config.MENU_URL || 'http://smarketing.20.249.184.228.nip.io/api/menu', // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) - SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', + SALES_URL: config.SALES_URL || 'http://smarketing.20.249.184.228.nip.io/api/sales', // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) - RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations' + RECOMMEND_URL: config.RECOMMEND_URL || 'http://smarketing.20.249.184.228.nip.io/api/recommendations' } } From 23b05807fa55b11a1920f71f381efe8a9238db6a Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 16:53:15 +0900 Subject: [PATCH 35/36] refactor: index --- index.html | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index e81c5da..2f10da1 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,59 @@ +//* public/index.html - + - - Vite App + + + + + + + + + + + + + + + + + + + + + + + + + + AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션
+ + + + + From 83f8ed730cd43f43a4dfd01b374aa83129d098f8 Mon Sep 17 00:00:00 2001 From: OhSeongRak Date: Wed, 18 Jun 2025 17:17:22 +0900 Subject: [PATCH 36/36] refactor: index --- index.html | 1 - public/index.html | 1 - 2 files changed, 2 deletions(-) diff --git a/index.html b/index.html index 2f10da1..7dc5153 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,3 @@ -//* public/index.html diff --git a/public/index.html b/public/index.html index 2f10da1..7dc5153 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,3 @@ -//* public/index.html