Merge branch 'main' of https://github.com/won-ktds/smarketing-frontend
This commit is contained in:
commit
92f1e16a69
422
deployment/Jenkinsfile
vendored
422
deployment/Jenkinsfile
vendored
@ -1,4 +1,5 @@
|
|||||||
// deployment/Jenkinsfile
|
// deployment/Jenkinsfile_ArgoCD
|
||||||
|
|
||||||
def PIPELINE_ID = "${env.BUILD_NUMBER}"
|
def PIPELINE_ID = "${env.BUILD_NUMBER}"
|
||||||
|
|
||||||
def getImageTag() {
|
def getImageTag() {
|
||||||
@ -13,148 +14,327 @@ podTemplate(
|
|||||||
containers: [
|
containers: [
|
||||||
containerTemplate(name: 'node', image: 'node:20-slim', ttyEnabled: true, command: 'cat'),
|
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: '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: 'git', image: 'alpine/git:latest', command: 'cat', ttyEnabled: true)
|
||||||
containerTemplate(name: 'envsubst', image: "hiondal/envsubst", command: 'sleep', args: '1h')
|
|
||||||
],
|
],
|
||||||
volumes: [
|
volumes: [
|
||||||
emptyDirVolume(mountPath: '/root/.azure', memory: false),
|
|
||||||
emptyDirVolume(mountPath: '/run/podman', memory: false)
|
emptyDirVolume(mountPath: '/run/podman', memory: false)
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
node(PIPELINE_ID) {
|
node(PIPELINE_ID) {
|
||||||
def props
|
def props
|
||||||
def imageTag = getImageTag()
|
def imageTag = getImageTag()
|
||||||
def manifest = "deploy.yaml"
|
|
||||||
def namespace
|
|
||||||
|
|
||||||
stage("Get Source") {
|
// Manifest Repository 설정
|
||||||
checkout scm
|
def MANIFEST_REPO = 'https://github.com/won-ktds/smarketing-manifest.git'
|
||||||
|
def MANIFEST_CREDENTIAL_ID = 'github-credentials-smarketing'
|
||||||
|
|
||||||
// 환경변수 파일 확인 및 읽기
|
try {
|
||||||
if (!fileExists('deployment/deploy_env_vars')) {
|
stage("Get Source") {
|
||||||
error "deployment/deploy_env_vars 파일이 없습니다!"
|
checkout scm
|
||||||
|
|
||||||
|
// 환경변수 파일 확인 및 읽기
|
||||||
|
if (!fileExists('deployment/deploy_env_vars')) {
|
||||||
|
error "deployment/deploy_env_vars 파일이 없습니다!"
|
||||||
|
}
|
||||||
|
|
||||||
|
props = readProperties file: "deployment/deploy_env_vars"
|
||||||
|
|
||||||
|
// 필수 환경변수 검증
|
||||||
|
if (!props.registry || !props.image_org || !props.namespace) {
|
||||||
|
error "필수 환경변수가 누락되었습니다. registry, image_org, namespace를 확인하세요."
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Build Information ==="
|
||||||
|
echo "Service: smarketing-frontend"
|
||||||
|
echo "Image Tag: ${imageTag}"
|
||||||
|
echo "Registry: ${props.registry}"
|
||||||
|
echo "Image Org: ${props.image_org}"
|
||||||
|
echo "Namespace: ${props.namespace}"
|
||||||
}
|
}
|
||||||
|
|
||||||
props = readProperties file: "deployment/deploy_env_vars"
|
stage("Check Changes") {
|
||||||
namespace = "${props.namespace}"
|
script {
|
||||||
|
def changes = sh(
|
||||||
|
script: "git diff --name-only HEAD~1 HEAD",
|
||||||
|
returnStdout: true
|
||||||
|
).trim()
|
||||||
|
|
||||||
// 필수 환경변수 검증
|
if (!changes.contains("src/") && !changes.contains("public/") && !changes.contains("package")) {
|
||||||
if (!props.registry || !props.image_org || !props.namespace) {
|
echo "No significant frontend changes detected, skipping build"
|
||||||
error "필수 환경변수가 누락되었습니다. registry, image_org, namespace를 확인하세요."
|
currentBuild.result = 'SUCCESS'
|
||||||
|
error("Stopping pipeline - no frontend changes detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Frontend changes detected, proceeding with build"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Registry: ${props.registry}"
|
stage('Build & Push Frontend Image') {
|
||||||
echo "Image Org: ${props.image_org}"
|
container('podman') {
|
||||||
echo "Namespace: ${namespace}"
|
sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
|
||||||
echo "Image Tag: ${imageTag}"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Setup AKS") {
|
withCredentials([usernamePassword(
|
||||||
container('azure-cli') {
|
credentialsId: 'acr-credentials',
|
||||||
withCredentials([azureServicePrincipal('azure-credentials')]) {
|
usernameVariable: 'ACR_USERNAME',
|
||||||
sh """
|
passwordVariable: 'ACR_PASSWORD'
|
||||||
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
|
def imagePath = "${props.registry}/${props.image_org}/smarketing-frontend:${imageTag}"
|
||||||
kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
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 "✅ Frontend image pushed successfully: ${imagePath}"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Update Manifest Repository') {
|
||||||
|
container('git') {
|
||||||
|
script {
|
||||||
|
// Manifest Repository Clone
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: MANIFEST_CREDENTIAL_ID,
|
||||||
|
usernameVariable: 'GIT_USERNAME',
|
||||||
|
passwordVariable: 'GIT_PASSWORD'
|
||||||
|
)]) {
|
||||||
|
sh """
|
||||||
|
echo "=== Git 설정 ==="
|
||||||
|
git config --global user.name "Jenkins CI"
|
||||||
|
git config --global user.email "jenkins@company.com"
|
||||||
|
|
||||||
|
echo "=== Manifest Repository Clone ==="
|
||||||
|
rm -rf manifest-repo
|
||||||
|
git clone https://\$GIT_USERNAME:\$GIT_PASSWORD@github.com/won-ktds/smarketing-manifest.git manifest-repo
|
||||||
|
cd manifest-repo
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fullImageName = "${props.registry}/${props.image_org}/smarketing-frontend:${imageTag}"
|
||||||
|
def deploymentFile = "smarketing-frontend/deployments/frontend-deployment.yaml"
|
||||||
|
|
||||||
|
sh """
|
||||||
|
cd manifest-repo
|
||||||
|
|
||||||
|
echo "=== smarketing-frontend 이미지 태그 업데이트 ==="
|
||||||
|
if [ -f "${deploymentFile}" ]; then
|
||||||
|
# 이미지 태그 업데이트 (sed 사용)
|
||||||
|
sed -i 's|image: ${props.registry}/${props.image_org}/smarketing-frontend:.*|image: ${fullImageName}|g' "${deploymentFile}"
|
||||||
|
echo "Updated ${deploymentFile} with new image: ${fullImageName}"
|
||||||
|
|
||||||
|
# 변경사항 확인
|
||||||
|
echo "=== 변경된 내용 확인 ==="
|
||||||
|
grep "image: ${props.registry}/${props.image_org}/smarketing-frontend" "${deploymentFile}" || echo "이미지 태그 업데이트 확인 실패"
|
||||||
|
else
|
||||||
|
echo "Warning: ${deploymentFile} not found"
|
||||||
|
echo "Creating manifest directory structure..."
|
||||||
|
|
||||||
|
# 기본 구조 생성
|
||||||
|
mkdir -p smarketing-frontend/deployments
|
||||||
|
|
||||||
|
# 기본 deployment 파일 생성
|
||||||
|
cat > "${deploymentFile}" << EOF
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-config
|
||||||
|
namespace: ${props.namespace}
|
||||||
|
data:
|
||||||
|
runtime-env.js: |
|
||||||
|
window.__runtime_config__ = {
|
||||||
|
AUTH_URL: '${props.auth_url}',
|
||||||
|
MEMBER_URL: '${props.member_url}',
|
||||||
|
STORE_URL: '${props.store_url}',
|
||||||
|
MENU_URL: '${props.menu_url}',
|
||||||
|
SALES_URL: '${props.sales_url}',
|
||||||
|
CONTENT_URL: '${props.content_url}',
|
||||||
|
RECOMMEND_URL: '${props.recommend_url}',
|
||||||
|
GATEWAY_URL: 'http://${props.ingress_host}',
|
||||||
|
ENV: 'production',
|
||||||
|
DEBUG: false
|
||||||
|
};
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend
|
||||||
|
namespace: ${props.namespace}
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
spec:
|
||||||
|
replicas: ${props.replicas}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: acr-secret
|
||||||
|
containers:
|
||||||
|
- name: smarketing-frontend
|
||||||
|
image: ${fullImageName}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: ${props.export_port}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: ${props.resources_requests_cpu}
|
||||||
|
memory: ${props.resources_requests_memory}
|
||||||
|
limits:
|
||||||
|
cpu: ${props.resources_limits_cpu}
|
||||||
|
memory: ${props.resources_limits_memory}
|
||||||
|
volumeMounts:
|
||||||
|
- name: runtime-config
|
||||||
|
mountPath: /usr/share/nginx/html/runtime-env.js
|
||||||
|
subPath: runtime-env.js
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: ${props.export_port}
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: ${props.export_port}
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
volumes:
|
||||||
|
- name: runtime-config
|
||||||
|
configMap:
|
||||||
|
name: smarketing-frontend-config
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-service
|
||||||
|
namespace: ${props.namespace}
|
||||||
|
labels:
|
||||||
|
app: smarketing-frontend
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: ${props.export_port}
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: smarketing-frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: smarketing-frontend-ingress
|
||||||
|
namespace: ${props.namespace}
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: ${props.ingress_host}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: smarketing-frontend-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
EOF
|
||||||
|
echo "Created basic frontend-deployment.yaml"
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
|
||||||
|
sh """
|
||||||
|
cd manifest-repo
|
||||||
|
|
||||||
|
echo "=== Git 변경사항 확인 ==="
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
|
||||||
|
# 변경사항이 있으면 커밋 및 푸시
|
||||||
|
if [ -n "\$(git status --porcelain)" ]; then
|
||||||
|
git add .
|
||||||
|
git commit -m "Update smarketing-frontend to ${imageTag} - Build ${env.BUILD_NUMBER}"
|
||||||
|
git push origin main
|
||||||
|
echo "✅ Successfully updated manifest repository"
|
||||||
|
else
|
||||||
|
echo "ℹ️ No changes to commit"
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Trigger ArgoCD Sync') {
|
||||||
|
script {
|
||||||
|
echo """
|
||||||
|
🎯 Frontend CI Pipeline 완료!
|
||||||
|
|
||||||
|
📦 빌드된 이미지:
|
||||||
|
- ${props.registry}/${props.image_org}/smarketing-frontend:${imageTag}
|
||||||
|
|
||||||
|
🔄 ArgoCD 동작:
|
||||||
|
- ArgoCD가 manifest repository 변경사항을 자동으로 감지합니다
|
||||||
|
- smarketing-frontend Application이 새로운 이미지로 동기화됩니다
|
||||||
|
- ArgoCD UI에서 배포 상태를 모니터링하세요
|
||||||
|
|
||||||
|
🌐 ArgoCD UI: [ArgoCD 접속 URL]
|
||||||
|
📁 Manifest Repo: ${MANIFEST_REPO}
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
stage('Build & Push Image') {
|
// 성공 시 처리
|
||||||
|
echo """
|
||||||
|
✅ Frontend CI Pipeline 성공!
|
||||||
|
🏷️ 새로운 이미지 태그: ${imageTag}
|
||||||
|
🔄 ArgoCD가 자동으로 배포를 시작합니다
|
||||||
|
"""
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 실패 시 처리
|
||||||
|
echo "❌ Frontend CI Pipeline 실패: ${e.getMessage()}"
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
// 정리 작업 (항상 실행)
|
||||||
container('podman') {
|
container('podman') {
|
||||||
sh 'podman system service -t 0 unix:///run/podman/podman.sock & sleep 2'
|
sh 'podman system prune -f || true'
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
}
|
}
|
||||||
|
sh 'rm -rf manifest-repo || true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,136 +0,0 @@
|
|||||||
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 파이프라인 중 오류가 발생했습니다."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
160
deployment/Jenkinsfile_backup
Normal file
160
deployment/Jenkinsfile_backup
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
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,9 +15,9 @@ export_port=18080
|
|||||||
ingress_host=smarketing.20.249.184.228.nip.io
|
ingress_host=smarketing.20.249.184.228.nip.io
|
||||||
|
|
||||||
# 리소스 설정 (프론트엔드에 맞게 조정)
|
# 리소스 설정 (프론트엔드에 맞게 조정)
|
||||||
resources_requests_cpu=128m # 프론트엔드는 CPU 사용량이 적음
|
resources_requests_cpu=128m
|
||||||
resources_requests_memory=128Mi # 메모리도 적게 사용
|
resources_requests_memory=128Mi
|
||||||
resources_limits_cpu=512m # 제한도 낮게 설정
|
resources_limits_cpu=512m
|
||||||
resources_limits_memory=512Mi
|
resources_limits_memory=512Mi
|
||||||
|
|
||||||
# API URLs (⭐ smarketing-backend ingress를 통해 라우팅)
|
# API URLs (⭐ smarketing-backend ingress를 통해 라우팅)
|
||||||
|
|||||||
@ -52,7 +52,7 @@ window.__runtime_config__ = {
|
|||||||
'http://localhost:8083/api/content',
|
'http://localhost:8083/api/content',
|
||||||
|
|
||||||
RECOMMEND_URL: isProduction() ?
|
RECOMMEND_URL: isProduction() ?
|
||||||
`${baseUrl}/api/recommend` :
|
`${baseUrl}/api/recommendations` :
|
||||||
'http://localhost:8084/api/recommendations',
|
'http://localhost:8084/api/recommendations',
|
||||||
|
|
||||||
// Gateway URL
|
// Gateway URL
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* AI 마케팅 서비스 - 메인 앱 진입점
|
* AI 마케팅 서비스 - 메인 앱 진입점
|
||||||
* Vue 3 + Vuetify 3 기반 애플리케이션 초기화
|
* Vue 3 + Vuetify 3 기반 애플리케이션 초기화
|
||||||
*/
|
*
|
||||||
|
*/
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|||||||
@ -7,14 +7,14 @@ const getApiUrls = () => {
|
|||||||
const config = window.__runtime_config__ || {}
|
const config = window.__runtime_config__ || {}
|
||||||
return {
|
return {
|
||||||
GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3',
|
GATEWAY_URL: config.GATEWAY_URL || 'http://20.1.2.3',
|
||||||
AUTH_URL: config.AUTH_URL || 'http://localhost:8081/api/auth',
|
AUTH_URL: config.AUTH_URL || 'http://smarketing.20.249.184.228.nip.io/api/auth',
|
||||||
MEMBER_URL: config.MEMBER_URL || 'http://localhost:8081/api/member',
|
MEMBER_URL: config.MEMBER_URL || 'http://smarketing.20.249.184.228.nip.io/api/member',
|
||||||
STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store',
|
STORE_URL: config.STORE_URL || 'http://smarketing.20.249.184.228.nip.io/api/store',
|
||||||
CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content',
|
CONTENT_URL: config.CONTENT_URL || 'http://smarketing.20.249.184.228.nip.io/api/content',
|
||||||
MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu',
|
MENU_URL: config.MENU_URL || 'http://smarketing.20.249.184.228.nip.io/api/menu',
|
||||||
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',
|
||||||
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',
|
||||||
IMAGE_URL: config.IMAGE_URL || 'http://localhost:8082/api/images'
|
IMAGE_URL: config.IMAGE_URL || 'http://smarketing.20.249.184.228.nip.io/api/images'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -333,7 +333,7 @@ class ContentService {
|
|||||||
|
|
||||||
// ✅ API 호출
|
// ✅ API 호출
|
||||||
const response = await contentApi.post('/sns/generate', formData, {
|
const response = await contentApi.post('/sns/generate', formData, {
|
||||||
timeout: 30000,
|
timeout: 0,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//* src/views/ContentCreationView.vue - 수정된 완전한 파일
|
//* src/views/ContentCreationView.vue - 완전 통합 버전
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="pa-0" style="height: 100vh; overflow: hidden;">
|
<v-container fluid class="pa-0" style="height: 100vh; overflow: hidden;">
|
||||||
@ -112,8 +112,21 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
|
|
||||||
<!-- 홍보 대상 -->
|
<!-- 홍보 대상 선택 (SNS) / 음식명 입력 (포스터) -->
|
||||||
|
<v-text-field
|
||||||
|
v-if="selectedType === 'poster'"
|
||||||
|
v-model="formData.menuName"
|
||||||
|
label="메뉴명"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="menuNameRules"
|
||||||
|
required
|
||||||
|
density="compact"
|
||||||
|
class="mb-3"
|
||||||
|
placeholder="예: 치킨 마요 덮밥, 딸기 라떼"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-select
|
<v-select
|
||||||
|
v-else
|
||||||
v-model="formData.targetType"
|
v-model="formData.targetType"
|
||||||
:items="getTargetTypes(selectedType)"
|
:items="getTargetTypes(selectedType)"
|
||||||
:label="selectedType === 'poster' ? '포스터 대상' : '홍보 대상'"
|
:label="selectedType === 'poster' ? '포스터 대상' : '홍보 대상'"
|
||||||
@ -122,11 +135,40 @@
|
|||||||
required
|
required
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
/>
|
@update:model-value="handleTargetTypeChange"
|
||||||
|
>
|
||||||
|
<template v-slot:item="{ props, item }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
:disabled="selectedType === 'poster' && item.value !== 'menu'"
|
||||||
|
:class="{ 'v-list-item--disabled': selectedType === 'poster' && item.value !== 'menu' }"
|
||||||
|
@click="handleTargetItemClick(item.value, $event)"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon
|
||||||
|
:color="(selectedType === 'poster' && item.value !== 'menu') ? 'grey-lighten-2' : 'primary'"
|
||||||
|
>
|
||||||
|
mdi-target
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title
|
||||||
|
:class="{ 'text-grey-lighten-1': selectedType === 'poster' && item.value !== 'menu' }"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle
|
||||||
|
v-if="selectedType === 'poster' && item.value !== 'menu'"
|
||||||
|
class="text-caption text-grey-lighten-1"
|
||||||
|
>
|
||||||
|
현재 메뉴만 지원
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
|
||||||
<!-- 이벤트명 -->
|
<!-- 이벤트명 (SNS에서 이벤트 선택 시) -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="formData.targetType === 'event'"
|
v-if="selectedType === 'sns' && formData.targetType === 'event'"
|
||||||
v-model="formData.eventName"
|
v-model="formData.eventName"
|
||||||
label="이벤트명"
|
label="이벤트명"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -142,26 +184,28 @@
|
|||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.promotionStartDate"
|
v-model="formData.promotionStartDate"
|
||||||
label="홍보 시작일"
|
label="홍보 시작일"
|
||||||
type="datetime-local"
|
type="date"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
:rules="promotionStartDateRules"
|
:rules="promotionStartDateRules"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="6">
|
<v-col cols="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.promotionEndDate"
|
v-model="formData.promotionEndDate"
|
||||||
label="홍보 종료일"
|
label="홍보 종료일"
|
||||||
type="datetime-local"
|
type="date"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
:rules="promotionEndDateRules"
|
:rules="promotionEndDateRules"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- 이벤트 기간 (이벤트인 경우) -->
|
<!-- 이벤트 기간 (SNS에서 이벤트인 경우) -->
|
||||||
<v-row v-if="formData.targetType === 'event'">
|
<v-row v-if="selectedType === 'sns' && formData.targetType === 'event'">
|
||||||
<v-col cols="6">
|
<v-col cols="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.startDate"
|
v-model="formData.startDate"
|
||||||
@ -272,8 +316,8 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
:disabled="!canGenerate || remainingGenerations <= 0 || contentStore.generating"
|
:disabled="!canGenerate || remainingGenerations <= 0 || isGenerating"
|
||||||
:loading="contentStore.generating"
|
:loading="isGenerating"
|
||||||
@click="generateContent"
|
@click="generateContent"
|
||||||
class="px-8"
|
class="px-8"
|
||||||
>
|
>
|
||||||
@ -388,7 +432,7 @@
|
|||||||
|
|
||||||
<!-- 콘텐츠 내용 -->
|
<!-- 콘텐츠 내용 -->
|
||||||
<div class="text-body-2 mb-3" style="line-height: 1.6;">
|
<div class="text-body-2 mb-3" style="line-height: 1.6;">
|
||||||
<!-- ✅ 포스터인 경우 이미지로 표시 -->
|
<!-- 포스터인 경우 이미지로 표시 -->
|
||||||
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
||||||
<v-img
|
<v-img
|
||||||
v-if="currentVersion.posterImage || currentVersion.content"
|
v-if="currentVersion.posterImage || currentVersion.content"
|
||||||
@ -421,7 +465,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ SNS인 경우 기존 텍스트 표시 -->
|
<!-- SNS인 경우 기존 텍스트 표시 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="isHtmlContent(currentVersion.content)"
|
<div v-if="isHtmlContent(currentVersion.content)"
|
||||||
class="html-content preview-content">
|
class="html-content preview-content">
|
||||||
@ -467,7 +511,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@click="copyToClipboard(currentVersion.content)"
|
@click="copyFullContent(currentVersion)"
|
||||||
>
|
>
|
||||||
<v-icon class="mr-1">mdi-content-copy</v-icon>
|
<v-icon class="mr-1">mdi-content-copy</v-icon>
|
||||||
복사
|
복사
|
||||||
@ -504,11 +548,11 @@
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-text class="pa-4" style="max-height: 500px;">
|
<v-card-text class="pa-4" style="max-height: 500px;">
|
||||||
<!-- ✅ 포스터인 경우 이미지 표시, SNS인 경우 텍스트 표시 -->
|
<!-- 포스터인 경우 이미지 표시, SNS인 경우 텍스트 표시 -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="text-h6 mb-2">콘텐츠</h4>
|
<h4 class="text-h6 mb-2">콘텐츠</h4>
|
||||||
|
|
||||||
<!-- ✅ 포스터인 경우 이미지로 표시 -->
|
<!-- 포스터인 경우 이미지로 표시 -->
|
||||||
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
||||||
<v-img
|
<v-img
|
||||||
v-if="currentVersion.posterImage || currentVersion.content"
|
v-if="currentVersion.posterImage || currentVersion.content"
|
||||||
@ -547,7 +591,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ✅ SNS인 경우 기존 텍스트 표시 -->
|
<!-- SNS인 경우 기존 텍스트 표시 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="isHtmlContent(currentVersion.content)"
|
<div v-if="isHtmlContent(currentVersion.content)"
|
||||||
class="pa-3 bg-grey-lighten-5 rounded html-content"
|
class="pa-3 bg-grey-lighten-5 rounded html-content"
|
||||||
@ -594,13 +638,19 @@
|
|||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-title>홍보 대상</v-list-item-title>
|
<v-list-item-title>홍보 대상</v-list-item-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
{{ currentVersion.targetType }}
|
{{ currentVersion.targetType || '메뉴' }}
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item v-if="currentVersion.eventName">
|
<v-list-item v-if="currentVersion.eventName || formData.eventName">
|
||||||
<v-list-item-title>이벤트명</v-list-item-title>
|
<v-list-item-title>이벤트명</v-list-item-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
{{ currentVersion.eventName }}
|
{{ currentVersion.eventName || formData.eventName }}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item v-if="currentVersion.menuName || formData.menuName">
|
||||||
|
<v-list-item-title>메뉴명</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
{{ currentVersion.menuName || formData.menuName }}
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
@ -639,7 +689,7 @@
|
|||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- 로딩 오버레이 -->
|
<!-- 로딩 오버레이 -->
|
||||||
<v-overlay v-model="contentStore.generating" contained persistent class="d-flex align-center justify-center">
|
<v-overlay v-model="isGenerating" contained persistent class="d-flex align-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<v-progress-circular color="primary" indeterminate size="64" class="mb-4" />
|
<v-progress-circular color="primary" indeterminate size="64" class="mb-4" />
|
||||||
<h3 class="text-h6 text-white mb-2">AI가 콘텐츠를 생성 중입니다</h3>
|
<h3 class="text-h6 text-white mb-2">AI가 콘텐츠를 생성 중입니다</h3>
|
||||||
@ -664,23 +714,25 @@ const router = useRouter()
|
|||||||
const contentStore = useContentStore()
|
const contentStore = useContentStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
// ✅ 반응형 데이터 - isGenerating 추가
|
// 반응형 데이터
|
||||||
const selectedType = ref('sns')
|
const selectedType = ref('sns')
|
||||||
const uploadedFiles = ref([])
|
const uploadedFiles = ref([])
|
||||||
const previewImages = ref([])
|
const previewImages = ref([])
|
||||||
const isPublishing = ref(false)
|
const isPublishing = ref(false)
|
||||||
const isGenerating = ref(false) // ✅ 추가
|
const isGenerating = ref(false)
|
||||||
const publishingIndex = ref(-1)
|
const publishingIndex = ref(-1)
|
||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const selectedVersion = ref(0)
|
const selectedVersion = ref(0)
|
||||||
const generatedVersions = ref([])
|
const generatedVersions = ref([])
|
||||||
const remainingGenerations = ref(3)
|
const remainingGenerations = ref(3)
|
||||||
|
const formValid = ref(false)
|
||||||
|
|
||||||
// 폼 데이터
|
// 폼 데이터
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
title: '',
|
title: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
targetType: '',
|
targetType: '',
|
||||||
|
menuName: '',
|
||||||
eventName: '',
|
eventName: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
@ -713,7 +765,7 @@ const contentTypes = [
|
|||||||
{
|
{
|
||||||
value: 'sns',
|
value: 'sns',
|
||||||
label: 'SNS 게시물',
|
label: 'SNS 게시물',
|
||||||
description: '인스타그램, 페이스북 등',
|
description: '인스타그램, 네이버블로그 등',
|
||||||
icon: 'mdi-instagram',
|
icon: 'mdi-instagram',
|
||||||
color: 'pink'
|
color: 'pink'
|
||||||
},
|
},
|
||||||
@ -728,15 +780,13 @@ const contentTypes = [
|
|||||||
|
|
||||||
const platformOptions = [
|
const platformOptions = [
|
||||||
{ title: '인스타그램', value: 'instagram' },
|
{ title: '인스타그램', value: 'instagram' },
|
||||||
{ title: '네이버 블로그', value: 'naver_blog' },
|
{ title: '네이버 블로그', value: 'naver_blog' }
|
||||||
{ title: '페이스북', value: 'facebook' },
|
|
||||||
{ title: '카카오스토리', value: 'kakao_story' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const targetTypes = [
|
const targetTypes = [
|
||||||
{ title: '메뉴', value: 'menu' },
|
{ title: '메뉴', value: 'menu' },
|
||||||
{ title: '매장', value: 'store' },
|
{ title: '매장', value: 'store' },
|
||||||
{ title: '이벤트', value: 'event' },
|
{ title: '이벤트', value: 'event' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 타겟 연령층 옵션
|
// 타겟 연령층 옵션
|
||||||
@ -754,13 +804,34 @@ const getTargetTypes = (type) => {
|
|||||||
if (type === 'poster') {
|
if (type === 'poster') {
|
||||||
return [
|
return [
|
||||||
{ title: '메뉴', value: 'menu' },
|
{ title: '메뉴', value: 'menu' },
|
||||||
{ title: '이벤트', value: 'event' },
|
|
||||||
{ title: '매장', value: 'store' },
|
{ title: '매장', value: 'store' },
|
||||||
|
{ title: '이벤트', value: 'event' },
|
||||||
{ title: '서비스', value: 'service' },
|
{ title: '서비스', value: 'service' },
|
||||||
{ title: '할인혜택', value: 'discount' }
|
{ title: '할인혜택', value: 'discount' }
|
||||||
]
|
]
|
||||||
} else {
|
}
|
||||||
return targetTypes
|
// SNS
|
||||||
|
return [
|
||||||
|
{ title: '메뉴', value: 'menu' },
|
||||||
|
{ title: '매장', value: 'store' },
|
||||||
|
{ title: '이벤트', value: 'event' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포스터 대상 선택 제한 함수들 (첫 번째 파일에서 추가)
|
||||||
|
const handleTargetItemClick = (value, event) => {
|
||||||
|
if (selectedType.value === 'poster' && value !== 'menu') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
appStore.showSnackbar('현재 포스터는 메뉴 대상만 지원됩니다.', 'warning')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTargetTypeChange = (value) => {
|
||||||
|
if (selectedType.value === 'poster' && value !== 'menu') {
|
||||||
|
formData.value.targetType = 'menu'
|
||||||
|
appStore.showSnackbar('현재 포스터는 메뉴 대상만 지원됩니다.', 'warning')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,6 +849,11 @@ const targetRules = [
|
|||||||
v => !!v || '홍보 대상을 선택해주세요'
|
v => !!v || '홍보 대상을 선택해주세요'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const menuNameRules = [
|
||||||
|
v => !!v || '메뉴명은 필수입니다',
|
||||||
|
v => (v && v.length <= 50) || '메뉴명은 50자 이하로 입력해주세요'
|
||||||
|
]
|
||||||
|
|
||||||
const eventNameRules = [
|
const eventNameRules = [
|
||||||
v => !formData.value.targetType || formData.value.targetType !== 'event' || !!v || '이벤트명은 필수입니다'
|
v => !formData.value.targetType || formData.value.targetType !== 'event' || !!v || '이벤트명은 필수입니다'
|
||||||
]
|
]
|
||||||
@ -807,51 +883,18 @@ const promotionEndDateRules = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// ✅ Computed 속성들
|
// Computed 속성들
|
||||||
const formValid = computed(() => {
|
|
||||||
// 기본 필수 필드 검증
|
|
||||||
if (!formData.value.title || !formData.value.targetType) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SNS 타입인 경우 플랫폼 필수
|
|
||||||
if (selectedType.value === 'sns' && !formData.value.platform) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 타입인 경우 추가 검증
|
|
||||||
if (formData.value.targetType === 'event') {
|
|
||||||
if (!formData.value.eventName || !formData.value.startDate || !formData.value.endDate) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 포스터 타입인 경우 추가 검증
|
|
||||||
if (selectedType.value === 'poster') {
|
|
||||||
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 포스터는 이미지 필수
|
|
||||||
if (!previewImages.value || previewImages.value.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const canGenerate = computed(() => {
|
const canGenerate = computed(() => {
|
||||||
try {
|
try {
|
||||||
// 기본 조건들 확인
|
|
||||||
if (!formValid.value) return false
|
|
||||||
if (!selectedType.value) return false
|
if (!selectedType.value) return false
|
||||||
if (!formData.value.title) return false
|
if (!formData.value.title) return false
|
||||||
|
|
||||||
// SNS 타입인 경우 플랫폼 필수
|
// SNS 타입인 경우 플랫폼 필수
|
||||||
if (selectedType.value === 'sns' && !formData.value.platform) return false
|
if (selectedType.value === 'sns' && !formData.value.platform) return false
|
||||||
|
|
||||||
// 포스터 타입인 경우 이미지 필수 및 홍보 기간 필수
|
// 포스터 타입인 경우 음식명과 이미지, 홍보 기간 필수
|
||||||
if (selectedType.value === 'poster') {
|
if (selectedType.value === 'poster') {
|
||||||
|
if (!formData.value.menuName) return false
|
||||||
if (!previewImages.value || previewImages.value.length === 0) return false
|
if (!previewImages.value || previewImages.value.length === 0) return false
|
||||||
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) return false
|
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) return false
|
||||||
}
|
}
|
||||||
@ -876,19 +919,60 @@ const currentVersion = computed(() => {
|
|||||||
// 메서드
|
// 메서드
|
||||||
const selectContentType = (type) => {
|
const selectContentType = (type) => {
|
||||||
selectedType.value = type
|
selectedType.value = type
|
||||||
console.log(`${type} 타입 선택됨`)
|
console.log(`${type} 타입 선택됨 - 폼 데이터 초기화`)
|
||||||
|
|
||||||
|
// ✅ 폼 데이터만 초기화 (생성된 콘텐츠는 보존)
|
||||||
|
formData.value = {
|
||||||
|
title: '',
|
||||||
|
platform: '',
|
||||||
|
targetType: type === 'poster' ? 'menu' : '', // 포스터는 메뉴로 기본 설정
|
||||||
|
menuName: '',
|
||||||
|
eventName: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
content: '',
|
||||||
|
hashtags: [],
|
||||||
|
category: '기타',
|
||||||
|
targetAge: '20대',
|
||||||
|
promotionStartDate: '',
|
||||||
|
promotionEndDate: '',
|
||||||
|
requirements: '',
|
||||||
|
toneAndManner: '친근함',
|
||||||
|
emotionIntensity: '보통',
|
||||||
|
imageStyle: '모던',
|
||||||
|
promotionType: '할인 정보',
|
||||||
|
photoStyle: '밝고 화사한'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 이미지 업로드 상태도 초기화
|
||||||
|
uploadedFiles.value = []
|
||||||
|
previewImages.value = []
|
||||||
|
|
||||||
|
// ✅ AI 옵션도 초기화
|
||||||
|
aiOptions.value = {
|
||||||
|
toneAndManner: 'friendly',
|
||||||
|
promotion: 'general',
|
||||||
|
emotionIntensity: 'normal',
|
||||||
|
photoStyle: '밝고 화사한',
|
||||||
|
imageStyle: '모던',
|
||||||
|
targetAge: '20대',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 폼 데이터 초기화 완료:', {
|
||||||
|
type: type,
|
||||||
|
targetType: formData.value.targetType,
|
||||||
|
preservedVersions: generatedVersions.value.length
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileUpload = (files) => {
|
const handleFileUpload = (files) => {
|
||||||
console.log('📁 파일 업로드 이벤트:', files)
|
console.log('📁 파일 업로드 이벤트:', files)
|
||||||
|
|
||||||
// 파일이 없는 경우 처리
|
|
||||||
if (!files || (Array.isArray(files) && files.length === 0)) {
|
if (!files || (Array.isArray(files) && files.length === 0)) {
|
||||||
console.log('📁 파일이 없음 - 기존 이미지 유지')
|
console.log('📁 파일이 없음 - 기존 이미지 유지')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 배열로 변환
|
|
||||||
let fileArray = []
|
let fileArray = []
|
||||||
if (files instanceof FileList) {
|
if (files instanceof FileList) {
|
||||||
fileArray = Array.from(files)
|
fileArray = Array.from(files)
|
||||||
@ -901,10 +985,8 @@ const handleFileUpload = (files) => {
|
|||||||
|
|
||||||
console.log('📁 처리할 파일 개수:', fileArray.length)
|
console.log('📁 처리할 파일 개수:', fileArray.length)
|
||||||
|
|
||||||
// 기존 이미지 완전히 초기화 (중복 방지)
|
|
||||||
previewImages.value = []
|
previewImages.value = []
|
||||||
|
|
||||||
// 각 파일 개별 처리
|
|
||||||
fileArray.forEach((file, index) => {
|
fileArray.forEach((file, index) => {
|
||||||
if (file && file.type && file.type.startsWith('image/')) {
|
if (file && file.type && file.type.startsWith('image/')) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@ -912,11 +994,9 @@ const handleFileUpload = (files) => {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
console.log(`📁 파일 ${index + 1} 읽기 완료: ${file.name}`)
|
console.log(`📁 파일 ${index + 1} 읽기 완료: ${file.name}`)
|
||||||
|
|
||||||
// 중복 방지를 위해 기존에 같은 이름의 파일이 있는지 확인
|
|
||||||
const existingIndex = previewImages.value.findIndex(img => img.name === file.name && img.size === file.size)
|
const existingIndex = previewImages.value.findIndex(img => img.name === file.name && img.size === file.size)
|
||||||
|
|
||||||
if (existingIndex === -1) {
|
if (existingIndex === -1) {
|
||||||
// 새로운 파일이면 추가
|
|
||||||
previewImages.value.push({
|
previewImages.value.push({
|
||||||
file: file,
|
file: file,
|
||||||
url: e.target.result,
|
url: e.target.result,
|
||||||
@ -944,7 +1024,6 @@ const removeImage = (index) => {
|
|||||||
console.log('🗑️ 이미지 삭제:', index)
|
console.log('🗑️ 이미지 삭제:', index)
|
||||||
previewImages.value.splice(index, 1)
|
previewImages.value.splice(index, 1)
|
||||||
|
|
||||||
// 업로드된 파일 목록도 업데이트
|
|
||||||
if (uploadedFiles.value && uploadedFiles.value.length > index) {
|
if (uploadedFiles.value && uploadedFiles.value.length > index) {
|
||||||
const newFiles = Array.from(uploadedFiles.value)
|
const newFiles = Array.from(uploadedFiles.value)
|
||||||
newFiles.splice(index, 1)
|
newFiles.splice(index, 1)
|
||||||
@ -952,9 +1031,10 @@ const removeImage = (index) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. generateContent 함수 - 완전한 버전
|
||||||
const generateContent = async () => {
|
const generateContent = async () => {
|
||||||
if (!formValid.value) {
|
if (!formData.value.title?.trim()) {
|
||||||
appStore.showSnackbar('모든 필수 항목을 입력해주세요.', 'warning')
|
appStore.showSnackbar('제목을 입력해주세요.', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -963,6 +1043,13 @@ const generateContent = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 포스터의 경우 메뉴 대상만 허용하는 최종 검증
|
||||||
|
if (selectedType.value === 'poster' && formData.value.targetType !== 'menu') {
|
||||||
|
appStore.showSnackbar('포스터는 메뉴 대상만 생성 가능합니다.', 'warning')
|
||||||
|
formData.value.targetType = 'menu'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isGenerating.value = true
|
isGenerating.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -970,37 +1057,73 @@ const generateContent = async () => {
|
|||||||
console.log('📋 [UI] 폼 데이터:', formData.value)
|
console.log('📋 [UI] 폼 데이터:', formData.value)
|
||||||
console.log('📁 [UI] 이미지 데이터:', previewImages.value)
|
console.log('📁 [UI] 이미지 데이터:', previewImages.value)
|
||||||
|
|
||||||
// ✅ 매장 ID 가져오기
|
// 매장 ID 가져오기 - API 호출로 변경
|
||||||
let storeId = 1 // 기본값
|
let storeId = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// localStorage에서 매장 정보 조회 시도
|
const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL)
|
||||||
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
? window.__runtime_config__.STORE_URL
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
: 'http://localhost:8082/api/store'
|
||||||
|
|
||||||
if (storeInfo.storeId) {
|
const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token')
|
||||||
storeId = storeInfo.storeId
|
|
||||||
} else if (userInfo.storeId) {
|
if (!token) {
|
||||||
storeId = userInfo.storeId
|
throw new Error('인증 토큰이 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeResponse = await fetch(`${storeApiUrl}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storeResponse.ok) {
|
||||||
|
const storeData = await storeResponse.json()
|
||||||
|
storeId = storeData.data?.storeId
|
||||||
|
console.log('✅ 매장 정보 조회 성공, storeId:', storeId)
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId)
|
throw new Error(`매장 정보 조회 실패: ${storeResponse.status}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId)
|
console.error('❌ 매장 정보 조회 실패:', error)
|
||||||
|
|
||||||
|
// fallback: localStorage에서 이전에 저장된 매장 정보 확인
|
||||||
|
try {
|
||||||
|
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
||||||
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||||||
|
|
||||||
|
if (storeInfo.storeId) {
|
||||||
|
storeId = storeInfo.storeId
|
||||||
|
console.log('⚠️ fallback - localStorage에서 매장 ID 사용:', storeId)
|
||||||
|
} else if (userInfo.storeId) {
|
||||||
|
storeId = userInfo.storeId
|
||||||
|
console.log('⚠️ fallback - userInfo에서 매장 ID 사용:', storeId)
|
||||||
|
} else {
|
||||||
|
throw new Error('매장 정보를 찾을 수 없습니다. 매장 관리 페이지에서 매장을 등록해주세요.')
|
||||||
|
}
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('❌ fallback 실패:', fallbackError)
|
||||||
|
throw new Error('매장 정보를 찾을 수 없습니다. 매장 관리 페이지에서 매장을 등록해주세요.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storeId) {
|
||||||
|
throw new Error('매장 ID를 가져올 수 없습니다. 매장 관리 페이지에서 매장을 등록해주세요.')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🏪 [UI] 사용할 매장 ID:', storeId)
|
console.log('🏪 [UI] 사용할 매장 ID:', storeId)
|
||||||
|
|
||||||
// ✅ Base64 이미지 URL 추출
|
// Base64 이미지 URL 추출
|
||||||
const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || []
|
const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || []
|
||||||
console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls)
|
console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls)
|
||||||
|
|
||||||
// ✅ 포스터 타입의 경우 이미지 필수 검증
|
// 포스터 타입의 경우 이미지 필수 검증
|
||||||
if (selectedType.value === 'poster' && imageUrls.length === 0) {
|
if (selectedType.value === 'poster' && imageUrls.length === 0) {
|
||||||
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
|
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 콘텐츠 생성 데이터 구성
|
// 콘텐츠 생성 데이터 구성
|
||||||
const contentData = {
|
const contentData = {
|
||||||
title: formData.value.title,
|
title: formData.value.title,
|
||||||
platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'),
|
platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'),
|
||||||
@ -1016,22 +1139,27 @@ const generateContent = async () => {
|
|||||||
endDate: formData.value.endDate,
|
endDate: formData.value.endDate,
|
||||||
toneAndManner: formData.value.toneAndManner || '친근함',
|
toneAndManner: formData.value.toneAndManner || '친근함',
|
||||||
emotionIntensity: formData.value.emotionIntensity || '보통',
|
emotionIntensity: formData.value.emotionIntensity || '보통',
|
||||||
images: imageUrls, // ✅ Base64 이미지 URL 배열
|
images: imageUrls,
|
||||||
storeId: storeId // ✅ 매장 ID 추가
|
storeId: storeId
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 포스터 전용 필드 추가
|
// 포스터 전용 필드 추가
|
||||||
if (selectedType.value === 'poster') {
|
if (selectedType.value === 'poster') {
|
||||||
contentData.promotionStartDate = formData.value.promotionStartDate
|
contentData.menuName = formData.value.menuName.trim()
|
||||||
contentData.promotionEndDate = formData.value.promotionEndDate
|
contentData.targetAudience = aiOptions.value.targetAge || '20대'
|
||||||
contentData.imageStyle = formData.value.imageStyle || '모던'
|
contentData.category = '메뉴소개'
|
||||||
contentData.promotionType = formData.value.promotionType
|
|
||||||
contentData.photoStyle = formData.value.photoStyle || '밝고 화사한'
|
if (formData.value.promotionStartDate) {
|
||||||
|
contentData.promotionStartDate = new Date(formData.value.promotionStartDate).toISOString()
|
||||||
|
}
|
||||||
|
if (formData.value.promotionEndDate) {
|
||||||
|
contentData.promotionEndDate = new Date(formData.value.promotionEndDate).toISOString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📤 [UI] 생성 요청 데이터:', contentData)
|
console.log('📤 [UI] 생성 요청 데이터:', contentData)
|
||||||
|
|
||||||
// ✅ contentData 무결성 체크
|
// contentData 무결성 체크
|
||||||
if (!contentData || typeof contentData !== 'object') {
|
if (!contentData || typeof contentData !== 'object') {
|
||||||
throw new Error('콘텐츠 데이터 구성에 실패했습니다.')
|
throw new Error('콘텐츠 데이터 구성에 실패했습니다.')
|
||||||
}
|
}
|
||||||
@ -1041,7 +1169,7 @@ const generateContent = async () => {
|
|||||||
contentData.images = []
|
contentData.images = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Store 호출
|
// Store 호출
|
||||||
console.log('🚀 [UI] contentStore.generateContent 호출')
|
console.log('🚀 [UI] contentStore.generateContent 호출')
|
||||||
const generated = await contentStore.generateContent(contentData)
|
const generated = await contentStore.generateContent(contentData)
|
||||||
|
|
||||||
@ -1049,18 +1177,16 @@ const generateContent = async () => {
|
|||||||
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
|
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 포스터 생성 결과 처리 개선
|
// 포스터 생성 결과 처리 개선
|
||||||
let finalContent = ''
|
let finalContent = ''
|
||||||
let posterImageUrl = ''
|
let posterImageUrl = ''
|
||||||
|
|
||||||
if (selectedType.value === 'poster') {
|
if (selectedType.value === 'poster') {
|
||||||
// 포스터의 경우 generated.data에서 이미지 URL 추출
|
|
||||||
posterImageUrl = generated.data?.posterImage || generated.data?.content || generated.content || ''
|
posterImageUrl = generated.data?.posterImage || generated.data?.content || generated.content || ''
|
||||||
finalContent = posterImageUrl // content 필드에 이미지 URL 저장
|
finalContent = posterImageUrl
|
||||||
|
|
||||||
console.log('🖼️ [UI] 포스터 이미지 URL:', posterImageUrl)
|
console.log('🖼️ [UI] 포스터 이미지 URL:', posterImageUrl)
|
||||||
} else {
|
} else {
|
||||||
// SNS의 경우 기존 로직 유지
|
|
||||||
finalContent = generated.content || generated.data?.content || ''
|
finalContent = generated.content || generated.data?.content || ''
|
||||||
|
|
||||||
// SNS용 이미지 추가
|
// SNS용 이미지 추가
|
||||||
@ -1079,18 +1205,19 @@ const generateContent = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 생성된 콘텐츠 객체에 이미지 정보 포함
|
// 생성된 콘텐츠 객체에 이미지 정보 포함
|
||||||
const newContent = {
|
const newContent = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
...contentData,
|
...contentData,
|
||||||
content: finalContent,
|
content: finalContent,
|
||||||
posterImage: posterImageUrl, // 포스터 이미지 URL 별도 저장
|
posterImage: posterImageUrl,
|
||||||
hashtags: generated.hashtags || generated.data?.hashtags || [],
|
hashtags: generated.hashtags || generated.data?.hashtags || [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
uploadedImages: previewImages.value || [], // ✅ 업로드된 이미지 정보 보존
|
uploadedImages: previewImages.value || [],
|
||||||
images: imageUrls, // ✅ Base64 URL 보존
|
images: imageUrls,
|
||||||
platform: contentData.platform || 'POSTER'
|
platform: contentData.platform || 'POSTER',
|
||||||
|
menuName: formData.value.menuName || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
generatedVersions.value.push(newContent)
|
generatedVersions.value.push(newContent)
|
||||||
@ -1124,6 +1251,7 @@ const selectVersion = (index) => {
|
|||||||
selectedVersion.value = index
|
selectedVersion.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. saveVersion 함수 - 완전한 버전
|
||||||
const saveVersion = async (index) => {
|
const saveVersion = async (index) => {
|
||||||
isPublishing.value = true
|
isPublishing.value = true
|
||||||
publishingIndex.value = index
|
publishingIndex.value = index
|
||||||
@ -1133,10 +1261,38 @@ const saveVersion = async (index) => {
|
|||||||
|
|
||||||
console.log('💾 [UI] 저장할 버전 데이터:', version)
|
console.log('💾 [UI] 저장할 버전 데이터:', version)
|
||||||
|
|
||||||
// ✅ 매장 ID 가져오기
|
// 매장 ID 가져오기 - API 호출로 변경
|
||||||
let storeId = 1 // 기본값
|
let storeId = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL)
|
||||||
|
? window.__runtime_config__.STORE_URL
|
||||||
|
: 'http://localhost:8082/api/store'
|
||||||
|
|
||||||
|
const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('인증 토큰이 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeResponse = await fetch(`${storeApiUrl}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storeResponse.ok) {
|
||||||
|
const storeData = await storeResponse.json()
|
||||||
|
storeId = storeData.data?.storeId
|
||||||
|
console.log('✅ [저장] 매장 정보 조회 성공, storeId:', storeId)
|
||||||
|
} else {
|
||||||
|
throw new Error(`매장 정보 조회 실패: ${storeResponse.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [저장] 매장 정보 조회 실패:', error)
|
||||||
|
|
||||||
|
// fallback
|
||||||
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||||||
|
|
||||||
@ -1145,54 +1301,48 @@ const saveVersion = async (index) => {
|
|||||||
} else if (userInfo.storeId) {
|
} else if (userInfo.storeId) {
|
||||||
storeId = userInfo.storeId
|
storeId = userInfo.storeId
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId)
|
throw new Error('매장 정보를 찾을 수 없습니다.')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId)
|
|
||||||
|
if (!storeId) {
|
||||||
|
throw new Error('매장 ID를 가져올 수 없습니다.')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🏪 [UI] 사용할 매장 ID:', storeId)
|
console.log('🏪 [UI] 사용할 매장 ID:', storeId)
|
||||||
|
|
||||||
// ✅ 이미지 데이터 준비
|
// 이미지 데이터 준비
|
||||||
let imageUrls = []
|
let imageUrls = []
|
||||||
|
|
||||||
// 포스터의 경우 생성된 포스터 이미지 URL과 업로드된 이미지들을 포함
|
|
||||||
if (selectedType.value === 'poster') {
|
if (selectedType.value === 'poster') {
|
||||||
// 1. 생성된 포스터 이미지 URL 추가
|
|
||||||
if (version.posterImage) {
|
if (version.posterImage) {
|
||||||
imageUrls.push(version.posterImage)
|
imageUrls.push(version.posterImage)
|
||||||
console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage)
|
console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. previewImages에서 원본 이미지 URL 추가
|
|
||||||
if (previewImages.value && previewImages.value.length > 0) {
|
if (previewImages.value && previewImages.value.length > 0) {
|
||||||
const originalImages = previewImages.value.map(img => img.url).filter(url => url)
|
const originalImages = previewImages.value.map(img => img.url).filter(url => url)
|
||||||
imageUrls = [...imageUrls, ...originalImages]
|
imageUrls = [...imageUrls, ...originalImages]
|
||||||
console.log('💾 [UI] 원본 이미지들:', originalImages)
|
console.log('💾 [UI] 원본 이미지들:', originalImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. version에 저장된 이미지도 확인
|
|
||||||
if (version.uploadedImages && version.uploadedImages.length > 0) {
|
if (version.uploadedImages && version.uploadedImages.length > 0) {
|
||||||
const versionImages = version.uploadedImages.map(img => img.url).filter(url => url)
|
const versionImages = version.uploadedImages.map(img => img.url).filter(url => url)
|
||||||
imageUrls = [...imageUrls, ...versionImages]
|
imageUrls = [...imageUrls, ...versionImages]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. version.images도 확인
|
|
||||||
if (version.images && Array.isArray(version.images) && version.images.length > 0) {
|
if (version.images && Array.isArray(version.images) && version.images.length > 0) {
|
||||||
imageUrls = [...imageUrls, ...version.images]
|
imageUrls = [...imageUrls, ...version.images]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 중복 제거
|
|
||||||
imageUrls = [...new Set(imageUrls)]
|
imageUrls = [...new Set(imageUrls)]
|
||||||
|
|
||||||
console.log('💾 [UI] 포스터 최종 이미지 URL들:', imageUrls)
|
console.log('💾 [UI] 포스터 최종 이미지 URL들:', imageUrls)
|
||||||
|
|
||||||
// 이미지가 없으면 에러
|
|
||||||
if (!imageUrls || imageUrls.length === 0) {
|
if (!imageUrls || imageUrls.length === 0) {
|
||||||
throw new Error('포스터 저장을 위해 최소 1개의 이미지가 필요합니다.')
|
throw new Error('포스터 저장을 위해 최소 1개의 이미지가 필요합니다.')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SNS의 경우 선택적으로 이미지 포함
|
|
||||||
if (previewImages.value && previewImages.value.length > 0) {
|
if (previewImages.value && previewImages.value.length > 0) {
|
||||||
imageUrls = previewImages.value.map(img => img.url).filter(url => url)
|
imageUrls = previewImages.value.map(img => img.url).filter(url => url)
|
||||||
}
|
}
|
||||||
@ -1203,67 +1353,44 @@ const saveVersion = async (index) => {
|
|||||||
|
|
||||||
console.log('💾 [UI] 최종 이미지 URL들:', imageUrls)
|
console.log('💾 [UI] 최종 이미지 URL들:', imageUrls)
|
||||||
|
|
||||||
// ✅ 저장 데이터 구성 - 타입에 따라 다르게 처리
|
// 저장 데이터 구성 - 타입에 따라 다르게 처리
|
||||||
let saveData
|
let saveData
|
||||||
|
|
||||||
if (selectedType.value === 'poster') {
|
if (selectedType.value === 'poster') {
|
||||||
// 포스터용 데이터 구성 (PosterContentSaveRequest에 맞춤)
|
|
||||||
saveData = {
|
saveData = {
|
||||||
// 매장 ID
|
|
||||||
storeId: storeId,
|
storeId: storeId,
|
||||||
|
|
||||||
// 기본 콘텐츠 정보 - 포스터는 content에 이미지 URL 저장
|
|
||||||
title: version.title,
|
title: version.title,
|
||||||
content: version.posterImage || version.content, // 포스터 이미지 URL을 content에 저장
|
content: version.posterImage || version.content,
|
||||||
images: imageUrls, // 모든 관련 이미지들
|
images: imageUrls,
|
||||||
|
|
||||||
// 분류 정보
|
|
||||||
category: getCategory(version.targetType || formData.value.targetType),
|
category: getCategory(version.targetType || formData.value.targetType),
|
||||||
requirement: formData.value.requirements || `${version.title}에 대한 포스터를 만들어주세요`,
|
requirement: formData.value.requirements || `${version.title}에 대한 포스터를 만들어주세요`,
|
||||||
|
|
||||||
// 이벤트 정보
|
|
||||||
eventName: version.eventName || formData.value.eventName,
|
eventName: version.eventName || formData.value.eventName,
|
||||||
startDate: formData.value.startDate,
|
startDate: formData.value.startDate,
|
||||||
endDate: formData.value.endDate,
|
endDate: formData.value.endDate,
|
||||||
|
|
||||||
// 스타일 정보
|
|
||||||
photoStyle: formData.value.photoStyle || '밝고 화사한'
|
photoStyle: formData.value.photoStyle || '밝고 화사한'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SNS용 데이터 구성 (SnsContentSaveRequest에 맞춤)
|
|
||||||
saveData = {
|
saveData = {
|
||||||
// 매장 ID
|
|
||||||
storeId: storeId,
|
storeId: storeId,
|
||||||
|
|
||||||
// 필수 필드들
|
|
||||||
contentType: 'SNS',
|
contentType: 'SNS',
|
||||||
platform: version.platform || formData.value.platform || 'INSTAGRAM',
|
platform: version.platform || formData.value.platform || 'INSTAGRAM',
|
||||||
|
|
||||||
// 기본 콘텐츠 정보
|
|
||||||
title: version.title,
|
title: version.title,
|
||||||
content: version.content,
|
content: version.content,
|
||||||
hashtags: version.hashtags || [],
|
hashtags: version.hashtags || [],
|
||||||
images: imageUrls,
|
images: imageUrls,
|
||||||
|
|
||||||
// 분류 정보
|
|
||||||
category: getCategory(version.targetType || formData.value.targetType),
|
category: getCategory(version.targetType || formData.value.targetType),
|
||||||
requirement: formData.value.requirements || `${version.title}에 대한 SNS 게시물을 만들어주세요`,
|
requirement: formData.value.requirements || `${version.title}에 대한 SNS 게시물을 만들어주세요`,
|
||||||
toneAndManner: formData.value.toneAndManner || '친근함',
|
toneAndManner: formData.value.toneAndManner || '친근함',
|
||||||
emotionIntensity: formData.value.emotionIntensity || '보통',
|
emotionIntensity: formData.value.emotionIntensity || '보통',
|
||||||
|
|
||||||
// 이벤트 정보
|
|
||||||
eventName: version.eventName || formData.value.eventName,
|
eventName: version.eventName || formData.value.eventName,
|
||||||
startDate: formData.value.startDate,
|
startDate: formData.value.startDate,
|
||||||
endDate: formData.value.endDate,
|
endDate: formData.value.endDate,
|
||||||
|
|
||||||
// 상태 정보
|
|
||||||
status: 'PUBLISHED'
|
status: 'PUBLISHED'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('💾 [UI] 최종 저장 데이터:', saveData)
|
console.log('💾 [UI] 최종 저장 데이터:', saveData)
|
||||||
|
|
||||||
// ✅ 저장 실행
|
|
||||||
await contentStore.saveContent(saveData)
|
await contentStore.saveContent(saveData)
|
||||||
|
|
||||||
version.status = 'published'
|
version.status = 'published'
|
||||||
@ -1297,18 +1424,32 @@ const copyToClipboard = async (content) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 개선된 복사 기능 - 포스터와 SNS 구분하여 처리
|
||||||
const copyFullContent = async (version) => {
|
const copyFullContent = async (version) => {
|
||||||
try {
|
try {
|
||||||
let fullContent = ''
|
let fullContent = ''
|
||||||
|
|
||||||
if (isHtmlContent(version.content)) {
|
// 포스터인 경우 제목과 간단한 설명만 복사
|
||||||
fullContent += extractTextFromHtml(version.content)
|
if (selectedType.value === 'poster' || version.contentType === 'poster' || version.type === 'poster') {
|
||||||
|
fullContent = version.title || '포스터'
|
||||||
|
if (formData.value.requirements) {
|
||||||
|
fullContent += '\n\n' + formData.value.requirements
|
||||||
|
}
|
||||||
|
if (version.posterImage || version.content) {
|
||||||
|
fullContent += '\n\n포스터 이미지: ' + (version.posterImage || version.content)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fullContent += version.content
|
// SNS 콘텐츠인 경우 HTML 태그 제거하고 텍스트만 추출
|
||||||
}
|
if (isHtmlContent(version.content)) {
|
||||||
|
fullContent += extractTextFromHtml(version.content)
|
||||||
|
} else {
|
||||||
|
fullContent += version.content || ''
|
||||||
|
}
|
||||||
|
|
||||||
if (version.hashtags && version.hashtags.length > 0) {
|
// 해시태그 추가
|
||||||
fullContent += '\n\n' + version.hashtags.join(' ')
|
if (version.hashtags && version.hashtags.length > 0) {
|
||||||
|
fullContent += '\n\n' + version.hashtags.join(' ')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigator.clipboard.writeText(fullContent)
|
await navigator.clipboard.writeText(fullContent)
|
||||||
@ -1352,14 +1493,8 @@ const getPlatformColor = (platform) => {
|
|||||||
|
|
||||||
const getPlatformLabel = (platform) => {
|
const getPlatformLabel = (platform) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
'instagram': '인스타그램',
|
|
||||||
'naver_blog': '네이버 블로그',
|
|
||||||
'facebook': '페이스북',
|
|
||||||
'kakao_story': '카카오스토리',
|
|
||||||
'INSTAGRAM': '인스타그램',
|
'INSTAGRAM': '인스타그램',
|
||||||
'NAVER_BLOG': '네이버 블로그',
|
'NAVER_BLOG': '네이버 블로그',
|
||||||
'FACEBOOK': '페이스북',
|
|
||||||
'KAKAO_STORY': '카카오스토리',
|
|
||||||
'POSTER': '포스터'
|
'POSTER': '포스터'
|
||||||
}
|
}
|
||||||
return labels[platform] || platform
|
return labels[platform] || platform
|
||||||
@ -1407,11 +1542,28 @@ const isHtmlContent = (content) => {
|
|||||||
return /<[^>]+>/.test(content)
|
return /<[^>]+>/.test(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 개선된 HTML 텍스트 추출 함수
|
||||||
const extractTextFromHtml = (html) => {
|
const extractTextFromHtml = (html) => {
|
||||||
if (!html) return ''
|
if (!html) return ''
|
||||||
const tempDiv = document.createElement('div')
|
|
||||||
tempDiv.innerHTML = html
|
try {
|
||||||
return tempDiv.textContent || tempDiv.innerText || ''
|
// HTML 태그를 제거하고 텍스트만 추출
|
||||||
|
const textContent = html
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n') // <br> 태그를 줄바꿈으로
|
||||||
|
.replace(/<\/p>/gi, '\n\n') // </p> 태그를 두 줄바꿈으로
|
||||||
|
.replace(/<[^>]*>/g, '') // 모든 HTML 태그 제거
|
||||||
|
.replace(/ /g, ' ') // 를 공백으로
|
||||||
|
.replace(/&/g, '&') // & 를 &로
|
||||||
|
.replace(/</g, '<') // < 를 <로
|
||||||
|
.replace(/>/g, '>') // > 를 >로
|
||||||
|
.replace(/"/g, '"') // " 를 "로
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
return textContent
|
||||||
|
} catch (error) {
|
||||||
|
console.error('HTML 텍스트 추출 실패:', error)
|
||||||
|
return html
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncateHtmlContent = (html, maxLength) => {
|
const truncateHtmlContent = (html, maxLength) => {
|
||||||
@ -1445,7 +1597,43 @@ const handleImageError = (event) => {
|
|||||||
// 라이프사이클
|
// 라이프사이클
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('📱 콘텐츠 생성 페이지 로드됨')
|
console.log('📱 콘텐츠 생성 페이지 로드됨')
|
||||||
|
|
||||||
|
// 초기 상태 확인
|
||||||
|
console.log('🔍 초기 상태 확인:')
|
||||||
|
console.log('- selectedType:', selectedType.value)
|
||||||
|
console.log('- formData:', formData.value)
|
||||||
|
console.log('- previewImages:', previewImages.value)
|
||||||
|
console.log('- canGenerate 존재:', typeof canGenerate)
|
||||||
|
|
||||||
|
// 5초 후 상태 재확인
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('🔍 5초 후 상태:')
|
||||||
|
console.log('- formData.title:', formData.value.title)
|
||||||
|
console.log('- formData.menuName:', formData.value.menuName)
|
||||||
|
console.log('- canGenerate:', canGenerate?.value)
|
||||||
|
}, 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 실시간 formData 변화 감지
|
||||||
|
watch(() => formData.value, (newVal) => {
|
||||||
|
console.log('📝 formData 실시간 변경:', {
|
||||||
|
title: newVal.title,
|
||||||
|
menuName: newVal.menuName,
|
||||||
|
targetType: newVal.targetType,
|
||||||
|
promotionStartDate: newVal.promotionStartDate,
|
||||||
|
promotionEndDate: newVal.promotionEndDate
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// canGenerate 변화 감지
|
||||||
|
watch(canGenerate, (newVal) => {
|
||||||
|
console.log('🎯 canGenerate 변경:', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
// previewImages 변화 감지
|
||||||
|
watch(() => previewImages.value, (newVal) => {
|
||||||
|
console.log('📁 previewImages 변경:', newVal.length, '개')
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -1528,4 +1716,3 @@ onMounted(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -232,14 +232,7 @@
|
|||||||
<p class="text-caption text-grey-darken-1 mb-0">맞춤형 마케팅 제안</p>
|
<p class="text-caption text-grey-darken-1 mb-0">맞춤형 마케팅 제안</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-btn
|
|
||||||
icon="mdi-refresh"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
color="primary"
|
|
||||||
:loading="aiLoading"
|
|
||||||
@click="refreshAiRecommendation"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
@ -791,115 +784,146 @@ const updateDashboardMetrics = (salesData) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 차트 데이터 업데이트 (수정 - 핵심 차트 연동 로직)
|
* 차트 데이터 업데이트 (하드코딩된 7일 데이터 사용)
|
||||||
*/
|
*/
|
||||||
const updateChartData = (salesData) => {
|
const updateChartData = (salesData) => {
|
||||||
try {
|
try {
|
||||||
console.log('📊 [CHART] 차트 데이터 업데이트 시작:', salesData)
|
console.log('📊 [CHART] 차트 데이터 업데이트 시작:', salesData)
|
||||||
|
|
||||||
// yearSales 데이터가 있으면 차트 데이터 업데이트
|
// ⚠️ 하드코딩된 7일 매출 데이터 (실제 DB 데이터 기반)
|
||||||
if (salesData.yearSales && salesData.yearSales.length > 0) {
|
const hardcodedSalesData = [
|
||||||
// Sales 엔티티 배열을 차트 형식으로 변환
|
{ salesDate: '2025-06-14', salesAmount: 230000 },
|
||||||
const salesDataPoints = salesData.yearSales.slice(-7).map((sale, index) => {
|
{ salesDate: '2025-06-15', salesAmount: 640000 },
|
||||||
const date = new Date(sale.salesDate)
|
{ salesDate: '2025-06-16', salesAmount: 140000 },
|
||||||
const label = `${date.getMonth() + 1}/${date.getDate()}`
|
{ salesDate: '2025-06-17', salesAmount: 800000 },
|
||||||
const amount = Number(sale.salesAmount) / 10000 // 만원 단위로 변환
|
{ salesDate: '2025-06-18', salesAmount: 900000 },
|
||||||
const originalAmount = Number(sale.salesAmount) // 원화 단위 원본 저장
|
{ salesDate: '2025-06-19', salesAmount: 500000 },
|
||||||
|
{ salesDate: '2025-06-20', salesAmount: 1600000 }
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
console.log('📊 [CHART] 하드코딩된 7일 데이터 사용:', hardcodedSalesData)
|
||||||
label: index === salesData.yearSales.length - 1 ? '오늘' : label,
|
|
||||||
sales: Math.round(amount),
|
|
||||||
target: Math.round(amount * 1.1), // 목표는 실제 매출의 110%로 설정
|
|
||||||
date: sale.salesDate,
|
|
||||||
originalSales: originalAmount, // ⚠️ 원화 단위 원본 추가
|
|
||||||
originalTarget: Math.round(originalAmount * 1.1) // ⚠️ 원화 단위 목표 추가
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('📊 [CHART] 변환된 7일 데이터:', salesDataPoints)
|
// Sales 엔티티 배열을 차트 형식으로 변환
|
||||||
|
const salesDataPoints = hardcodedSalesData.map((sale, index) => {
|
||||||
|
const date = new Date(sale.salesDate)
|
||||||
|
const label = `${date.getMonth() + 1}/${date.getDate()}`
|
||||||
|
const amount = Number(sale.salesAmount) / 10000 // 만원 단위로 변환
|
||||||
|
const originalAmount = Number(sale.salesAmount) // 원화 단위 원본 저장
|
||||||
|
|
||||||
// 7일 차트 데이터 업데이트
|
// 마지막 날짜를 '오늘'로 표시
|
||||||
chartData.value['7d'] = salesDataPoints
|
const displayLabel = index === hardcodedSalesData.length - 1 ? '오늘' : label
|
||||||
originalChartData.value['7d'] = salesDataPoints // ⚠️ 원본 데이터 저장
|
|
||||||
|
|
||||||
// 30일/90일 데이터 생성 (실제 데이터 기반)
|
return {
|
||||||
if (salesData.yearSales.length >= 30) {
|
label: displayLabel,
|
||||||
// 30일 데이터를 주간으로 그룹화
|
sales: Math.round(amount),
|
||||||
const weeklyData = []
|
target: Math.round(amount * 1.1), // 목표는 실제 매출의 110%로 설정
|
||||||
for (let i = 0; i < 5; i++) {
|
date: sale.salesDate,
|
||||||
const weekStart = Math.max(0, salesData.yearSales.length - 35 + (i * 7))
|
originalSales: originalAmount,
|
||||||
const weekEnd = Math.min(salesData.yearSales.length, weekStart + 7)
|
originalTarget: Math.round(originalAmount * 1.1)
|
||||||
const weekSales = salesData.yearSales.slice(weekStart, weekEnd)
|
|
||||||
|
|
||||||
if (weekSales.length > 0) {
|
|
||||||
const totalAmount = weekSales.reduce((sum, sale) => sum + Number(sale.salesAmount), 0)
|
|
||||||
const avgAmount = totalAmount / weekSales.length / 10000 // 만원 단위
|
|
||||||
const originalAvgAmount = totalAmount / weekSales.length // 원화 단위
|
|
||||||
|
|
||||||
weeklyData.push({
|
|
||||||
label: i === 4 ? '이번주' : `${i + 1}주차`,
|
|
||||||
sales: Math.round(avgAmount),
|
|
||||||
target: Math.round(avgAmount * 1.1),
|
|
||||||
date: `Week ${i + 1}`,
|
|
||||||
originalSales: Math.round(originalAvgAmount), // ⚠️ 원화 단위 원본
|
|
||||||
originalTarget: Math.round(originalAvgAmount * 1.1) // ⚠️ 원화 단위 목표
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weeklyData.length > 0) {
|
|
||||||
chartData.value['30d'] = weeklyData
|
|
||||||
originalChartData.value['30d'] = weeklyData // ⚠️ 원본 데이터 저장
|
|
||||||
console.log('📊 [CHART] 30일(주간) 데이터 생성:', weeklyData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (salesData.yearSales.length >= 90) {
|
console.log('📊 [CHART] 변환된 7일 차트 데이터:', salesDataPoints)
|
||||||
// 90일 데이터를 월간으로 그룹화
|
|
||||||
const monthlyData = []
|
|
||||||
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']
|
|
||||||
|
|
||||||
// 최근 4개월 데이터 생성
|
// 7일 차트 데이터 업데이트
|
||||||
for (let i = 0; i < 4; i++) {
|
chartData.value['7d'] = salesDataPoints
|
||||||
const monthStart = Math.max(0, salesData.yearSales.length - 120 + (i * 30))
|
originalChartData.value['7d'] = salesDataPoints
|
||||||
const monthEnd = Math.min(salesData.yearSales.length, monthStart + 30)
|
|
||||||
const monthSales = salesData.yearSales.slice(monthStart, monthEnd)
|
|
||||||
|
|
||||||
if (monthSales.length > 0) {
|
// 30일 데이터 생성 (하드코딩 데이터 기반으로 주간 그룹화)
|
||||||
const totalAmount = monthSales.reduce((sum, sale) => sum + Number(sale.salesAmount), 0)
|
const weeklyData = [
|
||||||
const avgAmount = totalAmount / monthSales.length / 10000 // 만원 단위
|
{
|
||||||
const originalAvgAmount = totalAmount / monthSales.length // 원화 단위
|
label: '1주차',
|
||||||
|
sales: Math.round((230000 + 640000) / 2 / 10000), // 23 + 64 / 2 = 43.5만원
|
||||||
const currentMonth = new Date().getMonth()
|
target: Math.round((230000 + 640000) / 2 / 10000 * 1.1),
|
||||||
const monthIndex = (currentMonth - 3 + i + 12) % 12
|
date: 'Week 1',
|
||||||
|
originalSales: Math.round((230000 + 640000) / 2),
|
||||||
monthlyData.push({
|
originalTarget: Math.round((230000 + 640000) / 2 * 1.1)
|
||||||
label: i === 3 ? '이번달' : monthNames[monthIndex],
|
},
|
||||||
sales: Math.round(avgAmount * 10), // 월간은 10배 스케일
|
{
|
||||||
target: Math.round(avgAmount * 11),
|
label: '2주차',
|
||||||
date: `Month ${i + 1}`,
|
sales: Math.round((140000 + 800000) / 2 / 10000), // 14 + 80 / 2 = 47만원
|
||||||
originalSales: Math.round(originalAvgAmount * 10), // ⚠️ 원화 단위 원본
|
target: Math.round((140000 + 800000) / 2 / 10000 * 1.1),
|
||||||
originalTarget: Math.round(originalAvgAmount * 11) // ⚠️ 원화 단위 목표
|
date: 'Week 2',
|
||||||
})
|
originalSales: Math.round((140000 + 800000) / 2),
|
||||||
}
|
originalTarget: Math.round((140000 + 800000) / 2 * 1.1)
|
||||||
}
|
},
|
||||||
|
{
|
||||||
if (monthlyData.length > 0) {
|
label: '3주차',
|
||||||
chartData.value['90d'] = monthlyData
|
sales: Math.round((900000 + 500000) / 2 / 10000), // 90 + 50 / 2 = 70만원
|
||||||
originalChartData.value['90d'] = monthlyData // ⚠️ 원본 데이터 저장
|
target: Math.round((900000 + 500000) / 2 / 10000 * 1.1),
|
||||||
console.log('📊 [CHART] 90일(월간) 데이터 생성:', monthlyData)
|
date: 'Week 3',
|
||||||
}
|
originalSales: Math.round((900000 + 500000) / 2),
|
||||||
|
originalTarget: Math.round((900000 + 500000) / 2 * 1.1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '4주차',
|
||||||
|
sales: Math.round(1600000 / 10000), // 160만원
|
||||||
|
target: Math.round(1600000 / 10000 * 1.1),
|
||||||
|
date: 'Week 4',
|
||||||
|
originalSales: 1600000,
|
||||||
|
originalTarget: Math.round(1600000 * 1.1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '이번주',
|
||||||
|
sales: Math.round((230000 + 640000 + 140000 + 800000 + 900000 + 500000 + 1600000) / 7 / 10000), // 평균
|
||||||
|
target: Math.round((230000 + 640000 + 140000 + 800000 + 900000 + 500000 + 1600000) / 7 / 10000 * 1.1),
|
||||||
|
date: 'Week 5',
|
||||||
|
originalSales: Math.round((230000 + 640000 + 140000 + 800000 + 900000 + 500000 + 1600000) / 7),
|
||||||
|
originalTarget: Math.round((230000 + 640000 + 140000 + 800000 + 900000 + 500000 + 1600000) / 7 * 1.1)
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// 차트 다시 그리기
|
chartData.value['30d'] = weeklyData
|
||||||
nextTick(() => {
|
originalChartData.value['30d'] = weeklyData
|
||||||
drawChart()
|
console.log('📊 [CHART] 30일(주간) 데이터 생성:', weeklyData)
|
||||||
})
|
|
||||||
|
// 90일 데이터 생성 (월간 그룹화)
|
||||||
|
const monthlyData = [
|
||||||
|
{
|
||||||
|
label: '3월',
|
||||||
|
sales: Math.round((230000 + 640000) / 2 / 1000), // 천원 단위 (더 큰 스케일)
|
||||||
|
target: Math.round((230000 + 640000) / 2 / 1000 * 1.1),
|
||||||
|
date: 'Month 1',
|
||||||
|
originalSales: Math.round((230000 + 640000) / 2 * 10),
|
||||||
|
originalTarget: Math.round((230000 + 640000) / 2 * 11)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '4월',
|
||||||
|
sales: Math.round((140000 + 800000) / 2 / 1000),
|
||||||
|
target: Math.round((140000 + 800000) / 2 / 1000 * 1.1),
|
||||||
|
date: 'Month 2',
|
||||||
|
originalSales: Math.round((140000 + 800000) / 2 * 10),
|
||||||
|
originalTarget: Math.round((140000 + 800000) / 2 * 11)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '5월',
|
||||||
|
sales: Math.round((900000 + 500000) / 2 / 1000),
|
||||||
|
target: Math.round((900000 + 500000) / 2 / 1000 * 1.1),
|
||||||
|
date: 'Month 3',
|
||||||
|
originalSales: Math.round((900000 + 500000) / 2 * 10),
|
||||||
|
originalTarget: Math.round((900000 + 500000) / 2 * 11)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '이번달',
|
||||||
|
sales: Math.round(1600000 / 1000),
|
||||||
|
target: Math.round(1600000 / 1000 * 1.1),
|
||||||
|
date: 'Month 4',
|
||||||
|
originalSales: Math.round(1600000 * 10),
|
||||||
|
originalTarget: Math.round(1600000 * 11)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
chartData.value['90d'] = monthlyData
|
||||||
|
originalChartData.value['90d'] = monthlyData
|
||||||
|
console.log('📊 [CHART] 90일(월간) 데이터 생성:', monthlyData)
|
||||||
|
|
||||||
|
// 차트 다시 그리기
|
||||||
|
nextTick(() => {
|
||||||
|
drawChart()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📊 [CHART] 하드코딩된 차트 데이터 업데이트 완료')
|
||||||
|
|
||||||
console.log('📊 [CHART] 차트 데이터 업데이트 완료')
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ [CHART] yearSales 데이터가 없음, 기본 차트 유지')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [CHART] 차트 데이터 업데이트 실패:', error)
|
console.error('❌ [CHART] 차트 데이터 업데이트 실패:', error)
|
||||||
// 실패 시 기본 차트 데이터 유지
|
// 실패 시 기본 차트 데이터 유지
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
@keyup.enter="handleLogin"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 비밀번호 입력 -->
|
<!-- 비밀번호 입력 -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="credentials.password"
|
v-model="credentials.password"
|
||||||
label="비밀번호"
|
label="비밀번호"
|
||||||
@ -45,7 +45,6 @@
|
|||||||
@keyup.enter="handleLogin"
|
@keyup.enter="handleLogin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<!-- 로그인 옵션 -->
|
<!-- 로그인 옵션 -->
|
||||||
<div class="d-flex justify-space-between align-center mb-6">
|
<div class="d-flex justify-space-between align-center mb-6">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
@ -109,10 +108,10 @@
|
|||||||
<h3 class="text-subtitle-2 font-weight-bold mb-2">데모 계정 정보</h3>
|
<h3 class="text-subtitle-2 font-weight-bold mb-2">데모 계정 정보</h3>
|
||||||
<div class="demo-info">
|
<div class="demo-info">
|
||||||
<div class="text-body-2 mb-1">
|
<div class="text-body-2 mb-1">
|
||||||
<span class="font-weight-medium">아이디:</span> user01
|
<span class="font-weight-medium">아이디:</span> test
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body-2 mb-2">
|
<div class="text-body-2 mb-2">
|
||||||
<span class="font-weight-medium">비밀번호:</span> passw0rd
|
<span class="font-weight-medium">비밀번호:</span> test1234!
|
||||||
</div>
|
</div>
|
||||||
<v-btn size="small" color="info" variant="outlined" @click="fillDemoCredentials">
|
<v-btn size="small" color="info" variant="outlined" @click="fillDemoCredentials">
|
||||||
데모 계정 자동 입력
|
데모 계정 자동 입력
|
||||||
@ -328,8 +327,8 @@ const emailChecked = ref(false)
|
|||||||
|
|
||||||
// 로그인 자격 증명
|
// 로그인 자격 증명
|
||||||
const credentials = ref({
|
const credentials = ref({
|
||||||
username: 'user01',
|
username: 'test',
|
||||||
password: 'passw0rd',
|
password: 'test1234!',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 회원가입 데이터
|
// 회원가입 데이터
|
||||||
@ -410,8 +409,8 @@ const businessNumberRules = [
|
|||||||
|
|
||||||
// 로그인 관련 메서드
|
// 로그인 관련 메서드
|
||||||
const fillDemoCredentials = () => {
|
const fillDemoCredentials = () => {
|
||||||
credentials.value.username = 'user01'
|
credentials.value.username = 'test'
|
||||||
credentials.value.password = 'passw0rd'
|
credentials.value.password = 'test1234!'
|
||||||
loginError.value = ''
|
loginError.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -78,17 +78,42 @@
|
|||||||
<v-card-text class="pa-6">
|
<v-card-text class="pa-6">
|
||||||
<v-row>
|
<v-row>
|
||||||
<!-- 매장 이미지 -->
|
<!-- 매장 이미지 -->
|
||||||
|
<!-- 매장 이미지 섹션 -->
|
||||||
<v-col cols="12" md="4" class="text-center">
|
<v-col cols="12" md="4" class="text-center">
|
||||||
<v-avatar size="120" class="mb-3">
|
<div class="store-image-container mb-3">
|
||||||
<v-img
|
<!-- 매장 사진이 있을 때 -->
|
||||||
:src="storeInfo.imageUrl || '/images/store-placeholder.png'"
|
<v-avatar
|
||||||
alt="매장 이미지"
|
v-if="storeInfo.storeImage || storeInfo.imageUrl"
|
||||||
/>
|
size="120"
|
||||||
</v-avatar>
|
class="store-avatar"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
:src="storeInfo.storeImage || storeInfo.imageUrl"
|
||||||
|
alt="매장 이미지"
|
||||||
|
/>
|
||||||
|
</v-avatar>
|
||||||
|
|
||||||
|
<!-- 매장 사진이 없을 때 - 업종별 이모지 표시 -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="store-emoji-container d-flex align-center justify-center"
|
||||||
|
:style="{
|
||||||
|
width: '120px',
|
||||||
|
height: '120px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: getStoreColor(storeInfo.businessType),
|
||||||
|
fontSize: '48px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ getStoreEmoji(storeInfo.businessType) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="text-h6 font-weight-bold">{{ storeInfo.storeName }}</h3>
|
<h3 class="text-h6 font-weight-bold">{{ storeInfo.storeName }}</h3>
|
||||||
<p class="text-grey">{{ storeInfo.businessType }}</p>
|
<p class="text-grey">{{ storeInfo.businessType }}</p>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
|
||||||
<!-- 기본 정보 -->
|
<!-- 기본 정보 -->
|
||||||
<v-col cols="12" md="8">
|
<v-col cols="12" md="8">
|
||||||
<v-row>
|
<v-row>
|
||||||
@ -1666,6 +1691,110 @@ onMounted(async () => {
|
|||||||
showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error')
|
showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 업종별 이모지 반환 함수
|
||||||
|
const getStoreEmoji = (businessType) => {
|
||||||
|
const emojiMap = {
|
||||||
|
'카페': '☕',
|
||||||
|
'레스토랑': '🍽️',
|
||||||
|
'한식': '🍲',
|
||||||
|
'중식': '🥢',
|
||||||
|
'일식': '🍣',
|
||||||
|
'양식': '🍝',
|
||||||
|
'치킨': '🍗',
|
||||||
|
'피자': '🍕',
|
||||||
|
'햄버거': '🍔',
|
||||||
|
'분식': '🍜',
|
||||||
|
'베이커리': '🥐',
|
||||||
|
'디저트': '🧁',
|
||||||
|
'아이스크림': '🍦',
|
||||||
|
'술집': '🍺',
|
||||||
|
'바': '🍸',
|
||||||
|
'펜션': '🏠',
|
||||||
|
'호텔': '🏨',
|
||||||
|
'게스트하우스': '🏡',
|
||||||
|
'마트': '🛒',
|
||||||
|
'편의점': '🏪',
|
||||||
|
'미용실': '💇',
|
||||||
|
'네일샵': '💅',
|
||||||
|
'세탁소': '👔',
|
||||||
|
'약국': '💊',
|
||||||
|
'병원': '🏥',
|
||||||
|
'헬스장': '💪',
|
||||||
|
'학원': '📚',
|
||||||
|
'키즈카페': '🧸',
|
||||||
|
'반려동물': '🐾',
|
||||||
|
'꽃집': '🌸',
|
||||||
|
'문구점': '✏️',
|
||||||
|
'서점': '📖',
|
||||||
|
'화장품': '💄',
|
||||||
|
'옷가게': '👗',
|
||||||
|
'신발가게': '👟',
|
||||||
|
'가구점': '🪑',
|
||||||
|
'전자제품': '📱',
|
||||||
|
'자동차': '🚗',
|
||||||
|
'주유소': '⛽',
|
||||||
|
'세차장': '🚿',
|
||||||
|
'부동산': '🏢',
|
||||||
|
'은행': '🏦',
|
||||||
|
'우체국': '📮',
|
||||||
|
'기타': '🏪'
|
||||||
|
}
|
||||||
|
|
||||||
|
return emojiMap[businessType] || '🏪'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 업종별 배경색 반환 함수
|
||||||
|
const getStoreColor = (businessType) => {
|
||||||
|
const colorMap = {
|
||||||
|
'카페': '#8D6E63',
|
||||||
|
'레스토랑': '#FF7043',
|
||||||
|
'한식': '#D32F2F',
|
||||||
|
'중식': '#F57C00',
|
||||||
|
'일식': '#388E3C',
|
||||||
|
'양식': '#303F9F',
|
||||||
|
'치킨': '#FBC02D',
|
||||||
|
'피자': '#E64A19',
|
||||||
|
'햄버거': '#795548',
|
||||||
|
'분식': '#FF5722',
|
||||||
|
'베이커리': '#F57C00',
|
||||||
|
'디저트': '#E91E63',
|
||||||
|
'아이스크림': '#00BCD4',
|
||||||
|
'술집': '#FF9800',
|
||||||
|
'바': '#9C27B0',
|
||||||
|
'펜션': '#4CAF50',
|
||||||
|
'호텔': '#2196F3',
|
||||||
|
'게스트하우스': '#009688',
|
||||||
|
'마트': '#607D8B',
|
||||||
|
'편의점': '#3F51B5',
|
||||||
|
'미용실': '#E91E63',
|
||||||
|
'네일샵': '#9C27B0',
|
||||||
|
'세탁소': '#00BCD4',
|
||||||
|
'약국': '#4CAF50',
|
||||||
|
'병원': '#2196F3',
|
||||||
|
'헬스장': '#FF5722',
|
||||||
|
'학원': '#673AB7',
|
||||||
|
'키즈카페': '#FFEB3B',
|
||||||
|
'반려동물': '#795548',
|
||||||
|
'꽃집': '#E91E63',
|
||||||
|
'문구점': '#FF9800',
|
||||||
|
'서점': '#795548',
|
||||||
|
'화장품': '#E91E63',
|
||||||
|
'옷가게': '#9C27B0',
|
||||||
|
'신발가게': '#607D8B',
|
||||||
|
'가구점': '#8BC34A',
|
||||||
|
'전자제품': '#607D8B',
|
||||||
|
'자동차': '#424242',
|
||||||
|
'주유소': '#FF5722',
|
||||||
|
'세차장': '#00BCD4',
|
||||||
|
'부동산': '#795548',
|
||||||
|
'은행': '#2196F3',
|
||||||
|
'우체국': '#FF5722',
|
||||||
|
'기타': '#9E9E9E'
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorMap[businessType] || '#9E9E9E'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -2208,4 +2337,23 @@ onMounted(async () => {
|
|||||||
.store-dialog-content::-webkit-scrollbar-thumb:hover {
|
.store-dialog-content::-webkit-scrollbar-thumb:hover {
|
||||||
background: #a8a8a8;
|
background: #a8a8a8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-image-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-avatar {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-emoji-container {
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-emoji-container:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user