Merge branch 'main' into front
This commit is contained in:
commit
d85bbf903d
160
deployment/Jenkinsfile
vendored
Normal file
160
deployment/Jenkinsfile
vendored
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
// 환경변수 파일 확인 및 읽기
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
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') {
|
||||||
|
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 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 \\
|
||||||
|
-t ${imagePath} .
|
||||||
|
|
||||||
|
# 이미지 푸시
|
||||||
|
podman push ${imagePath}
|
||||||
|
|
||||||
|
echo "Image pushed successfully: ${imagePath}"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Generate & Apply Manifest') {
|
||||||
|
container('envsubst') {
|
||||||
|
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}
|
||||||
|
|
||||||
|
# API URLs도 export (혹시 사용할 수도 있으니)
|
||||||
|
export auth_url=${props.auth_url}
|
||||||
|
export member_url=${props.member_url}
|
||||||
|
export store_url=${props.store_url}
|
||||||
|
export menu_url=${props.menu_url}
|
||||||
|
export sales_url=${props.sales_url}
|
||||||
|
export content_url=${props.content_url}
|
||||||
|
export recommend_url=${props.recommend_url}
|
||||||
|
|
||||||
|
echo "=== 환경변수 확인 ==="
|
||||||
|
echo "namespace: \$namespace"
|
||||||
|
echo "ingress_host: \$ingress_host"
|
||||||
|
echo "export_port: \$export_port"
|
||||||
|
echo "========================="
|
||||||
|
|
||||||
|
envsubst < deployment/${manifest}.template > deployment/${manifest}
|
||||||
|
echo "Generated manifest file:"
|
||||||
|
cat deployment/${manifest}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
container('azure-cli') {
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
deployment/Jenkinsfile_ArgoCD
Normal file
136
deployment/Jenkinsfile_ArgoCD
Normal file
@ -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') {
|
||||||
|
steps {
|
||||||
|
container('podman') {
|
||||||
|
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') {
|
||||||
|
steps {
|
||||||
|
container('git') {
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: 'github-credentials-${env.TEAMID}',
|
||||||
|
usernameVariable: 'GIT_USERNAME',
|
||||||
|
passwordVariable: 'GIT_PASSWORD'
|
||||||
|
)]) {
|
||||||
|
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 파이프라인 중 오류가 발생했습니다."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
deployment/container/Dockerfile-smarketing-frontend
Normal file
58
deployment/container/Dockerfile-smarketing-frontend
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 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=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}
|
||||||
|
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
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY ${PROJECT_FOLDER}/package*.json ./
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
ARG EXPORT_PORT=18080
|
||||||
|
|
||||||
|
# Copy built files from builder stage
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 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;"]
|
||||||
97
deployment/container/nginx.conf
Normal file
97
deployment/container/nginx.conf
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
server {
|
||||||
|
listen 18080;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
# 에러 페이지 설정
|
||||||
|
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 debug;
|
||||||
|
|
||||||
|
# ⚠️ 중요: 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;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/xml+rss
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# 보안 헤더
|
||||||
|
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'; 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;
|
||||||
|
|
||||||
|
# 헬스체크 엔드포인트
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 파비콘 캐시
|
||||||
|
location /favicon.ico {
|
||||||
|
expires 1M;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 숨겨진 파일 접근 차단
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
deployment/deploy.yaml.template
Normal file
172
deployment/deploy.yaml.template
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
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: '${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}',
|
||||||
|
|
||||||
|
// 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] 런타임 설정 로드 완료 (배포 환경)');
|
||||||
|
|
||||||
|
---
|
||||||
|
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
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: ${export_port}
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: runtime-config
|
||||||
|
configMap:
|
||||||
|
name: smarketing-frontend-config
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-service
|
||||||
|
namespace: ${namespace}
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: ${export_port}
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: smarketing-frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-ingress
|
||||||
|
namespace: ${namespace}
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: ${ingress_host}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: smarketing-frontend-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
68
deployment/deploy_env_vars
Normal file
68
deployment/deploy_env_vars
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# deployment/deploy_env_vars
|
||||||
|
# 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=128m # 프론트엔드는 CPU 사용량이 적음
|
||||||
|
resources_requests_memory=128Mi # 메모리도 적게 사용
|
||||||
|
resources_limits_cpu=512m # 제한도 낮게 설정
|
||||||
|
resources_limits_memory=512Mi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Frontend 이미지 경로 설정
|
||||||
|
smarketing_frontend_image_path=${registry}/${image_org}/smarketing-frontend:latest
|
||||||
|
|
||||||
|
# GitHub 설정
|
||||||
|
github_org=won-ktds
|
||||||
|
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
|
||||||
89
deployment/manifest/deployment.yaml
Normal file
89
deployment/manifest/deployment.yaml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend
|
||||||
|
namespace: ${namespace}
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
version: v1
|
||||||
|
spec:
|
||||||
|
replicas: ${replicas}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
version: v1
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: acr-secret
|
||||||
|
containers:
|
||||||
|
- name: smarketing-frontend
|
||||||
|
image: ${smarketing_frontend_image_path}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: ${export_port}
|
||||||
|
name: http
|
||||||
|
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
|
||||||
|
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
|
||||||
|
defaultMode: 0644
|
||||||
|
# 배포 전략 설정
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 0
|
||||||
|
maxSurge: 1
|
||||||
|
# 재시작 정책
|
||||||
|
restartPolicy: Always
|
||||||
|
# DNS 정책
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
74
deployment/manifest/frontend-configmap.yaml
Normal file
74
deployment/manifest/frontend-configmap.yaml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-config
|
||||||
|
namespace: ${namespace}
|
||||||
|
data:
|
||||||
|
runtime-env.js: |
|
||||||
|
console.log('=== RUNTIME-ENV.JS LOADED (PRODUCTION) ===');
|
||||||
|
|
||||||
|
window.__runtime_config__ = {
|
||||||
|
// 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',
|
||||||
|
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/recommend',
|
||||||
|
|
||||||
|
// Gateway URL (production environment)
|
||||||
|
GATEWAY_URL: 'http://${ingress_host}',
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
|
FEATURES: {
|
||||||
|
ANALYTICS: true,
|
||||||
|
PUSH_NOTIFICATIONS: true,
|
||||||
|
SOCIAL_LOGIN: false,
|
||||||
|
MULTI_LANGUAGE: false,
|
||||||
|
API_HEALTH_CHECK: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Environment settings (production)
|
||||||
|
ENV: 'production',
|
||||||
|
DEBUG: false,
|
||||||
|
|
||||||
|
// 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('Missing required URL: ' + url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
return config[urlKey] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute validation
|
||||||
|
validateConfig();
|
||||||
|
console.log('Runtime config loaded successfully (PRODUCTION)');
|
||||||
15
deployment/manifest/service.yaml
Normal file
15
deployment/manifest/service.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-service
|
||||||
|
namespace: ${namespace}
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: ${export_port}
|
||||||
|
protocol: TCP
|
||||||
|
selector:
|
||||||
|
app: smarketing-frontend
|
||||||
51
index.html
51
index.html
@ -1,13 +1,58 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<meta name="description" content="소상공인을 위한 AI 마케팅 솔루션" />
|
||||||
|
<meta name="keywords" content="AI, 마케팅, 소상공인, 콘텐츠, 자동화" />
|
||||||
|
<meta name="author" content="AI 마케팅 팀" />
|
||||||
|
|
||||||
|
<!-- PWA 설정 -->
|
||||||
|
<meta name="theme-color" content="#1976D2" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="AI 마케팅" />
|
||||||
|
|
||||||
|
<!-- 파비콘 -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/logo192.png" />
|
||||||
|
|
||||||
|
<!-- Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
|
<!-- 폰트 사전 로드 -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 런타임 환경 설정 -->
|
||||||
|
<script src="/runtime-env.js"></script>
|
||||||
|
|
||||||
|
<title>AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<!-- 앱 스크립트 -->
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|
||||||
|
<!-- 서비스 워커 등록 -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('/sw.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('SW registered: ', registration)
|
||||||
|
})
|
||||||
|
.catch((registrationError) => {
|
||||||
|
console.log('SW registration failed: ', registrationError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "ai-marketing-frontend",
|
"name": "smarketing-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -21,6 +21,7 @@
|
|||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-vuetify": "^2.0.0",
|
"vite-plugin-vuetify": "^2.0.0",
|
||||||
"sass": "^1.69.0"
|
"sass": "^1.69.0",
|
||||||
|
"eslint": "^8.55.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
//* public/index.html
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
@ -29,14 +28,14 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>test</title>
|
<!-- 런타임 환경 설정 -->
|
||||||
|
<script src="/runtime-env.js"></script>
|
||||||
|
|
||||||
|
<title>AI 마케팅 - 소상공인을 위한 스마트 마케팅 솔루션</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<!-- 런타임 환경 설정 -->
|
|
||||||
<script src="/runtime-env.js"></script>
|
|
||||||
|
|
||||||
<!-- 앱 스크립트 -->
|
<!-- 앱 스크립트 -->
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|
||||||
|
|||||||
@ -1,44 +1,88 @@
|
|||||||
//* public/runtime-env.js - 백엔드 API 경로에 맞게 수정
|
//* public/runtime-env.js - Production environment priority configuration
|
||||||
console.log('=== RUNTIME-ENV.JS 로드됨 ===');
|
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');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default ingress host setting (from deploy_env_vars)
|
||||||
|
const DEFAULT_INGRESS_HOST = 'smarketing.20.249.184.228.nip.io';
|
||||||
|
|
||||||
|
// Environment-specific API URL configuration
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
if (isProduction()) {
|
||||||
|
// Production: use ingress host
|
||||||
|
return `http://${DEFAULT_INGRESS_HOST}`;
|
||||||
|
} else {
|
||||||
|
// Development: use localhost
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = getBaseUrl();
|
||||||
|
|
||||||
window.__runtime_config__ = {
|
window.__runtime_config__ = {
|
||||||
// ⚠️ 수정: 백엔드 API 구조에 맞게 URL 설정
|
// Use ingress host in production, localhost in development
|
||||||
AUTH_URL: 'http://localhost:8081/api/auth',
|
AUTH_URL: isProduction() ?
|
||||||
MEMBER_URL: 'http://localhost:8081/api/member',
|
`${baseUrl}/api/auth` :
|
||||||
STORE_URL: 'http://localhost:8082/api/store',
|
'http://localhost:8081/api/auth',
|
||||||
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', // ⚠️ 수정: 올바른 경로
|
|
||||||
|
|
||||||
// Gateway URL (운영 환경용)
|
MEMBER_URL: isProduction() ?
|
||||||
GATEWAY_URL: 'http://20.1.2.3',
|
`${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: isProduction() ? baseUrl : 'http://20.1.2.3',
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
ANALYTICS: true,
|
ANALYTICS: true,
|
||||||
PUSH_NOTIFICATIONS: true,
|
PUSH_NOTIFICATIONS: true,
|
||||||
SOCIAL_LOGIN: false,
|
SOCIAL_LOGIN: false,
|
||||||
MULTI_LANGUAGE: false,
|
MULTI_LANGUAGE: false,
|
||||||
API_HEALTH_CHECK: true, // ⚠️ 추가
|
API_HEALTH_CHECK: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 환경 설정
|
// Environment settings
|
||||||
ENV: 'development',
|
ENV: isProduction() ? 'production' : 'development',
|
||||||
DEBUG: true,
|
DEBUG: !isProduction(),
|
||||||
|
|
||||||
// ⚠️ 추가: API 타임아웃 설정
|
// API timeout settings
|
||||||
API_TIMEOUT: 30000,
|
API_TIMEOUT: 30000,
|
||||||
|
|
||||||
// ⚠️ 추가: 재시도 설정
|
// Retry settings
|
||||||
RETRY_ATTEMPTS: 3,
|
RETRY_ATTEMPTS: 3,
|
||||||
RETRY_DELAY: 1000,
|
RETRY_DELAY: 1000,
|
||||||
|
|
||||||
// 버전 정보
|
// Version information
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
BUILD_DATE: new Date().toISOString(),
|
BUILD_DATE: new Date().toISOString(),
|
||||||
|
|
||||||
// ⚠️ 추가: 백엔드 서비스 포트 정보 (디버깅용)
|
// Backend service port information (for debugging)
|
||||||
BACKEND_PORTS: {
|
BACKEND_PORTS: {
|
||||||
AUTH: 8081,
|
AUTH: 8081,
|
||||||
STORE: 8082,
|
STORE: 8082,
|
||||||
@ -47,39 +91,41 @@ window.__runtime_config__ = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⚠️ 추가: 설정 검증 함수
|
// Configuration validation function
|
||||||
const validateConfig = () => {
|
const validateConfig = () => {
|
||||||
const config = window.__runtime_config__;
|
const config = window.__runtime_config__;
|
||||||
const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL'];
|
const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL'];
|
||||||
|
|
||||||
for (const url of requiredUrls) {
|
for (const url of requiredUrls) {
|
||||||
if (!config[url]) {
|
if (!config[url]) {
|
||||||
console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`);
|
console.error(`Missing required URL: ${url}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ [CONFIG] 설정 검증 완료');
|
console.log('Config validation completed');
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⚠️ 추가: 개발 환경에서만 상세 로깅
|
// Environment-specific detailed logging
|
||||||
if (window.__runtime_config__.DEBUG) {
|
if (window.__runtime_config__.DEBUG) {
|
||||||
console.log('=== 백엔드 API URLs ===');
|
console.log('=== Current Environment Info ===');
|
||||||
console.log('🔐 AUTH_URL:', window.__runtime_config__.AUTH_URL);
|
console.log('Environment:', window.__runtime_config__.ENV);
|
||||||
console.log('🏪 STORE_URL:', window.__runtime_config__.STORE_URL);
|
console.log('Hostname:', window.location.hostname);
|
||||||
console.log('📊 SALES_URL:', window.__runtime_config__.SALES_URL);
|
console.log('Is Production:', isProduction());
|
||||||
console.log('🤖 RECOMMEND_URL:', window.__runtime_config__.RECOMMEND_URL);
|
|
||||||
console.log('📄 CONTENT_URL:', window.__runtime_config__.CONTENT_URL);
|
|
||||||
|
|
||||||
console.log('=== 설정 상세 정보 ===');
|
console.log('=== Backend API URLs ===');
|
||||||
console.log('전체 설정:', window.__runtime_config__);
|
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('=== Detailed Configuration ===');
|
||||||
validateConfig();
|
console.log('Full config:', window.__runtime_config__);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️ 추가: 전역 설정 접근 함수
|
// Global configuration access functions
|
||||||
window.getApiConfig = () => window.__runtime_config__;
|
window.getApiConfig = () => window.__runtime_config__;
|
||||||
window.getApiUrl = (serviceName) => {
|
window.getApiUrl = (serviceName) => {
|
||||||
const config = window.__runtime_config__;
|
const config = window.__runtime_config__;
|
||||||
@ -87,4 +133,7 @@ window.getApiUrl = (serviceName) => {
|
|||||||
return config[urlKey] || null;
|
return config[urlKey] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('✅ [RUNTIME] 런타임 설정 로드 완료');
|
// Execute configuration validation
|
||||||
|
validateConfig();
|
||||||
|
|
||||||
|
console.log(`Runtime configuration loaded successfully (${window.__runtime_config__.ENV} environment)`);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
//* src/services/menu.js - 백엔드 수정 없이 프론트엔드만 수정
|
//* src/services/menu.js - apiWithImage 제거하고 menuApi로 통일
|
||||||
import { menuApi, apiWithImage, handleApiError, formatSuccessResponse } from './api.js'
|
import { menuApi, handleApiError, formatSuccessResponse } from './api.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 관련 API 서비스
|
* 메뉴 관련 API 서비스
|
||||||
@ -48,7 +48,7 @@ class MenuService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 등록 (createMenu)
|
* 메뉴 등록
|
||||||
* @param {Object} menuData - 메뉴 정보
|
* @param {Object} menuData - 메뉴 정보
|
||||||
* @returns {Promise<Object>} 등록 결과
|
* @returns {Promise<Object>} 등록 결과
|
||||||
*/
|
*/
|
||||||
@ -79,95 +79,9 @@ class MenuService {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('메뉴 등록 실패:', error)
|
console.error('메뉴 등록 실패:', error)
|
||||||
return handleApiError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (error.response?.status === 400) {
|
||||||
* 메뉴 등록 (registerMenu 별칭)
|
const errorMessage = error.response.data?.message || 'validation 에러가 발생했습니다.'
|
||||||
* @param {Object} menuData - 메뉴 정보
|
|
||||||
* @returns {Promise<Object>} 등록 결과
|
|
||||||
*/
|
|
||||||
async registerMenu(menuData) {
|
|
||||||
return await this.createMenu(menuData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 수정
|
|
||||||
* @param {number} menuId - 메뉴 ID
|
|
||||||
* @param {Object} menuData - 수정할 메뉴 정보
|
|
||||||
* @returns {Promise<Object>} 수정 결과
|
|
||||||
*/
|
|
||||||
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 || '입력 데이터가 올바르지 않습니다.'
|
|
||||||
console.error('백엔드 validation 에러:', errorMessage)
|
console.error('백엔드 validation 에러:', errorMessage)
|
||||||
|
|
||||||
return {
|
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 || '서버 내부 오류가 발생했습니다.'
|
const errorMessage = error.response.data?.message || '서버 내부 오류가 발생했습니다.'
|
||||||
console.error('백엔드 에러 메시지:', errorMessage)
|
console.error('백엔드 에러 메시지:', errorMessage)
|
||||||
|
|
||||||
@ -188,15 +101,58 @@ async updateMenu(menuId, menuData) {
|
|||||||
error: error.response.data
|
error: error.response.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleApiError(error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 이미지 업로드
|
* 메뉴 수정
|
||||||
|
* @param {number} menuId - 메뉴 ID
|
||||||
|
* @param {Object} menuData - 수정할 메뉴 정보
|
||||||
|
* @returns {Promise<Object>} 수정 결과
|
||||||
|
*/
|
||||||
|
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 {number} menuId - 메뉴 ID
|
||||||
* @param {File} file - 이미지 파일
|
* @param {File} file - 이미지 파일
|
||||||
* @returns {Promise<Object>} 업로드 결과
|
* @returns {Promise<Object>} 업로드 결과
|
||||||
@ -225,8 +181,8 @@ async updateMenu(menuId, menuData) {
|
|||||||
|
|
||||||
console.log('이미지 업로드 요청 - 메뉴 ID:', numericMenuId)
|
console.log('이미지 업로드 요청 - 메뉴 ID:', numericMenuId)
|
||||||
|
|
||||||
// POST /api/images/menu/{menuId}
|
// POST /api/menu/images/{menuId} (menuApi 사용)
|
||||||
const response = await apiWithImage.post(`/images/menu/${numericMenuId}`, formData, {
|
const response = await menuApi.post(`/images/${numericMenuId}`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
@ -289,221 +245,3 @@ export default menuService
|
|||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
window.menuService = menuService
|
window.menuService = menuService
|
||||||
}
|
}
|
||||||
|
|
||||||
//* src/views/StoreManagementView.vue의 수정된 스크립트 부분
|
|
||||||
// <script setup> 내부의 메뉴 관련 함수들만 수정
|
|
||||||
|
|
||||||
// 메뉴 상세 보기 함수 - 메뉴 목록 데이터 활용
|
|
||||||
const viewMenuDetail = (menu) => {
|
|
||||||
console.log('=== 메뉴 상세 보기 호출 ===')
|
|
||||||
console.log('전달받은 메뉴 객체:', menu)
|
|
||||||
|
|
||||||
// 메뉴 ID 추출 (여러 형태 지원)
|
|
||||||
const menuId = menu.menuId || menu.id
|
|
||||||
|
|
||||||
if (!menuId) {
|
|
||||||
console.error('❌ 메뉴 ID를 찾을 수 없음:', menu)
|
|
||||||
showSnackbar('메뉴 정보가 올바르지 않습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 사용할 메뉴 ID:', menuId)
|
|
||||||
|
|
||||||
// API 호출 없이 바로 메뉴 목록의 데이터 사용
|
|
||||||
selectedMenu.value = {
|
|
||||||
...menu,
|
|
||||||
// 호환성을 위해 여러 형태 지원
|
|
||||||
id: menuId,
|
|
||||||
menuId: menuId,
|
|
||||||
name: menu.menuName || menu.name,
|
|
||||||
menuName: menu.menuName || menu.name
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 메뉴 상세 정보 설정 완료:', selectedMenu.value)
|
|
||||||
showMenuDetailDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 수정 함수
|
|
||||||
const editMenu = (menu) => {
|
|
||||||
console.log('=== 메뉴 수정 호출 ===')
|
|
||||||
console.log('전달받은 메뉴 객체:', menu)
|
|
||||||
|
|
||||||
// 메뉴 ID 추출 및 검증
|
|
||||||
const menuId = menu.menuId || menu.id
|
|
||||||
|
|
||||||
console.log('추출된 메뉴 ID:', menuId, '타입:', typeof menuId)
|
|
||||||
|
|
||||||
if (!menuId) {
|
|
||||||
console.error('❌ 메뉴 ID를 찾을 수 없음')
|
|
||||||
showSnackbar('메뉴 정보가 올바르지 않습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 사용할 메뉴 ID:', menuId)
|
|
||||||
|
|
||||||
// 수정 모드로 설정
|
|
||||||
menuEditMode.value = true
|
|
||||||
|
|
||||||
// 폼 데이터 설정 (여러 필드명 지원)
|
|
||||||
menuFormData.value = {
|
|
||||||
menuId: menuId,
|
|
||||||
id: menuId, // 호환성
|
|
||||||
menuName: menu.menuName || menu.name,
|
|
||||||
name: menu.menuName || menu.name, // 호환성
|
|
||||||
category: menu.category,
|
|
||||||
price: menu.price,
|
|
||||||
description: menu.description || '',
|
|
||||||
imageUrl: menu.imageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 수정 폼 데이터 설정 완료:', menuFormData.value)
|
|
||||||
|
|
||||||
// 다이얼로그 표시
|
|
||||||
showMenuDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 상세에서 수정 버튼 클릭
|
|
||||||
const editFromDetail = () => {
|
|
||||||
console.log('=== 메뉴 상세에서 수정 버튼 클릭 ===')
|
|
||||||
console.log('selectedMenu.value:', selectedMenu.value)
|
|
||||||
|
|
||||||
if (!selectedMenu.value) {
|
|
||||||
showSnackbar('선택된 메뉴가 없습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 ID 검증
|
|
||||||
const menuId = selectedMenu.value.menuId || selectedMenu.value.id
|
|
||||||
if (!menuId) {
|
|
||||||
showSnackbar('메뉴 ID를 찾을 수 없습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 수정할 메뉴 정보:', {
|
|
||||||
id: menuId,
|
|
||||||
name: selectedMenu.value.menuName || selectedMenu.value.name,
|
|
||||||
category: selectedMenu.value.category
|
|
||||||
})
|
|
||||||
|
|
||||||
// 상세 다이얼로그 닫기
|
|
||||||
closeMenuDetail()
|
|
||||||
|
|
||||||
// 수정 모드로 전환
|
|
||||||
editMenu(selectedMenu.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상세 다이얼로그 닫기
|
|
||||||
const closeMenuDetail = () => {
|
|
||||||
console.log('=== 메뉴 상세 다이얼로그 닫기 ===')
|
|
||||||
showMenuDetailDialog.value = false
|
|
||||||
selectedMenu.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 저장 함수 - 이미지 업로드 분리
|
|
||||||
const saveMenuWithImage = async () => {
|
|
||||||
if (saving.value) return
|
|
||||||
|
|
||||||
console.log('=== 메뉴 저장 + 이미지 업로드 시작 ===')
|
|
||||||
saving.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 메뉴 서비스 임포트
|
|
||||||
const { menuService } = await import('@/services/menu')
|
|
||||||
|
|
||||||
let menuResult
|
|
||||||
|
|
||||||
if (menuEditMode.value) {
|
|
||||||
// 메뉴 수정 - PUT /api/menu/{menuId}
|
|
||||||
const menuId = menuFormData.value.id || menuFormData.value.menuId
|
|
||||||
if (!menuId) {
|
|
||||||
showSnackbar('메뉴 ID가 없습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('메뉴 수정 API 호출, 메뉴 ID:', menuId)
|
|
||||||
|
|
||||||
// 메뉴 데이터 준비
|
|
||||||
const menuData = {
|
|
||||||
menuName: menuFormData.value.menuName || menuFormData.value.name,
|
|
||||||
category: menuFormData.value.category,
|
|
||||||
price: menuFormData.value.price,
|
|
||||||
description: menuFormData.value.description || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
menuResult = await menuService.updateMenu(menuId, menuData)
|
|
||||||
} else {
|
|
||||||
// 새 메뉴 등록 - POST /api/menu/register
|
|
||||||
const storeId = storeInfo.value?.storeId
|
|
||||||
if (!storeId) {
|
|
||||||
showSnackbar('매장 정보를 찾을 수 없습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 데이터 준비 (매장 ID 포함)
|
|
||||||
const menuData = {
|
|
||||||
storeId: storeId,
|
|
||||||
menuName: menuFormData.value.menuName || menuFormData.value.name,
|
|
||||||
category: menuFormData.value.category,
|
|
||||||
price: menuFormData.value.price,
|
|
||||||
description: menuFormData.value.description || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('메뉴 등록 API 호출, 매장 ID:', storeId)
|
|
||||||
menuResult = await menuService.createMenu(menuData)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ 메뉴 저장 완료:', menuResult)
|
|
||||||
|
|
||||||
if (!menuResult.success) {
|
|
||||||
showSnackbar(menuResult.message || '메뉴 저장에 실패했습니다', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 메뉴 저장 성공 후 이미지 업로드
|
|
||||||
let imageResult = { success: true }
|
|
||||||
|
|
||||||
if (selectedImageFile.value) {
|
|
||||||
console.log('=== 이미지 업로드 시작 ===')
|
|
||||||
|
|
||||||
// 등록된 메뉴의 ID 가져오기
|
|
||||||
const menuId = menuEditMode.value
|
|
||||||
? (menuFormData.value.id || menuFormData.value.menuId)
|
|
||||||
: menuResult.data?.menuId
|
|
||||||
|
|
||||||
if (menuId) {
|
|
||||||
console.log('이미지 업로드 - 메뉴 ID:', menuId)
|
|
||||||
imageResult = await menuService.uploadMenuImage(menuId, selectedImageFile.value)
|
|
||||||
console.log('이미지 업로드 결과:', imageResult)
|
|
||||||
|
|
||||||
if (!imageResult.success) {
|
|
||||||
console.warn('이미지 업로드는 실패했지만 메뉴는 저장됨')
|
|
||||||
showSnackbar('메뉴는 저장되었지만 이미지 업로드에 실패했습니다', 'warning')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('메뉴 ID를 찾을 수 없어 이미지 업로드 생략')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
if (menuResult.success && imageResult.success) {
|
|
||||||
showSnackbar(
|
|
||||||
menuEditMode.value ? '메뉴가 수정되었습니다' : '메뉴가 등록되었습니다',
|
|
||||||
'success'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다이얼로그 닫기 및 초기화
|
|
||||||
showMenuDialog.value = false
|
|
||||||
menuEditMode.value = false
|
|
||||||
resetMenuForm()
|
|
||||||
|
|
||||||
// 메뉴 목록 새로고침
|
|
||||||
await loadMenus()
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('메뉴 저장 중 오류:', error)
|
|
||||||
showSnackbar('저장 중 오류가 발생했습니다', 'error')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +1,36 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vuetify from 'vite-plugin-vuetify'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vuetify({
|
||||||
|
autoImport: true,
|
||||||
|
})
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
}
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
host: true,
|
|
||||||
open: true,
|
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: true,
|
sourcemap: false,
|
||||||
rollupOptions: {
|
minify: 'esbuild' // terser 대신 esbuild 사용 (더 빠르고 별도 설치 불필요)
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
vendor: ['vue', 'vue-router', 'pinia'],
|
|
||||||
vuetify: ['vuetify'],
|
|
||||||
icons: ['@mdi/font'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: true
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user