모든 localhost API 엔드포인트를 Gateway URL로 변경

- 모든 API 클라이언트에서 localhost 참조 제거
- Gateway URL 하드코딩: http://kt-event-marketing-api.20.214.196.128.nip.io
- 프로덕션/개발 환경 구분 제거
- 런타임 설정 로직 제거
- Dockerfile 및 배포 설정 추가
This commit is contained in:
cherry2250 2025-10-30 14:05:07 +09:00
parent e65ee14d61
commit f6f6e450cd
24 changed files with 1004 additions and 33 deletions

47
.dockerignore Normal file
View File

@ -0,0 +1,47 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Next.js
.next
out
# Testing
coverage
.nyc_output
playwright-report
test-results
# Misc
.DS_Store
*.pem
# Debug
*.log
# Local env files
.env*.local
.env
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
# Git
.git
.gitignore
# IDE
.vscode
.idea
*.swp
*.swo
# Deployment
deployment/k8s
claude
claudedocs

83
.serena/project.yml Normal file
View File

@ -0,0 +1,83 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran go
# haskell java julia kotlin lua markdown
# nix perl php python python_jedi r
# rego ruby ruby_solargraph rust scala swift
# terraform typescript typescript_vts zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "fe-kt-event-marketing"

47
Dockerfile Normal file
View File

@ -0,0 +1,47 @@
# Stage 1: Build Stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build Next.js application
RUN npm run build
# Stage 2: Production Stage with Nginx
FROM nginx:alpine
# Install Node.js for Next.js standalone mode
RUN apk add --no-cache nodejs
# Copy nginx configuration
COPY deployment/container/nginx.conf /etc/nginx/nginx.conf
# Copy built Next.js application from builder
COPY --from=builder /app/.next/standalone /app
COPY --from=builder /app/.next/static /app/.next/static
COPY --from=builder /app/public /app/public
# Create health check endpoint
RUN echo '<!DOCTYPE html><html><body><h1>OK</h1></body></html>' > /usr/share/nginx/html/health.html
# Copy runtime-env.js template (will be replaced by ConfigMap in K8s)
COPY public/runtime-env.js /usr/share/nginx/html/runtime-env.js
# Create startup script
RUN echo '#!/bin/sh' > /start.sh && \
echo 'cd /app && HOSTNAME=0.0.0.0 PORT=3000 node server.js &' >> /start.sh && \
echo 'sleep 3' >> /start.sh && \
echo 'nginx -g "daemon off;"' >> /start.sh && \
chmod +x /start.sh
EXPOSE 8080
CMD ["/bin/sh", "/start.sh"]

View File

@ -0,0 +1,79 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/rss+xml font/truetype font/opentype
application/vnd.ms-fontobject image/svg+xml;
server {
listen 8080;
server_name _;
# Health check endpoint
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# Proxy to Next.js standalone server
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files
location /_next/static {
proxy_pass http://localhost:3000;
proxy_cache_valid 200 60m;
add_header Cache-Control "public, max-age=3600, immutable";
}
# Public files
location /runtime-env.js {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
}

View File

@ -0,0 +1,20 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-kt-event-marketing-frontend
namespace: kt-event-marketing
data:
runtime-env.js: |
// 런타임 환경 설정 (배포 시 동적으로 주입 가능)
window.__runtime_config__ = {
API_GROUP: "/api/v1",
// 7개 마이크로서비스 호스트
USER_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
EVENT_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
CONTENT_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
AI_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
PARTICIPATION_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
DISTRIBUTION_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
ANALYTICS_HOST: "http://kt-event-marketing-api.20.214.196.128.nip.io",
};

View File

@ -0,0 +1,396 @@
# KT 이벤트 마케팅 프론트엔드 Kubernetes 배포 가이드
## 1. 배포 정보
### 실행 정보
- **시스템명**: kt-event-marketing-frontend
- **서비스명**: kt-event-marketing-frontend
- **ACR명**: acrdigitalgarage01
- **k8s명**: aks-digitalgarage-01
- **네임스페이스**: kt-event-marketing
- **파드수**: 1
- **리소스(CPU)**: 256m/1024m
- **리소스(메모리)**: 256Mi/1024Mi
- **Gateway Host**: http://kt-event-marketing-api.20.214.196.128.nip.io
### 배포 URL
- **프론트엔드 접속 URL**: http://kt-event-marketing-frontend.20.214.196.128.nip.io
---
## 2. 배포 검증 결과
### ✅ 체크리스트 검증 완료
#### 객체 이름 네이밍 룰 준수
- ✅ Ingress: `kt-event-marketing-frontend`
- ✅ ConfigMap: `cm-kt-event-marketing-frontend`
- ✅ Service: `kt-event-marketing-frontend`
- ✅ Deployment: `kt-event-marketing-frontend`
#### Ingress Controller External IP 확인
```bash
kubectl get svc ingress-nginx-controller -n ingress-nginx
```
**결과**:
```
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.0.76.134 20.214.196.128 80:32094/TCP,443:30210/TCP 204d
```
- ✅ External IP: `20.214.196.128`
- ✅ Ingress Host: `kt-event-marketing-frontend.20.214.196.128.nip.io`
#### 포트 일치성 확인
- ✅ Ingress의 backend.service.port.number: `8080`
- ✅ Service의 port: `8080`
- ✅ Service의 targetPort: `8080`
#### 이미지명 확인
- ✅ 이미지명: `acrdigitalgarage01.azurecr.io/kt-event-marketing-frontend/kt-event-marketing-frontend:latest`
#### ConfigMap 데이터 확인
- ✅ ConfigMap 이름: `cm-kt-event-marketing-frontend`
- ✅ Key: `runtime-env.js`
- ✅ Value: 모든 백엔드 API 호스트가 `http://kt-event-marketing-api.20.214.196.128.nip.io`로 설정됨
- USER_HOST
- EVENT_HOST
- CONTENT_HOST
- AI_HOST
- PARTICIPATION_HOST
- DISTRIBUTION_HOST
- ANALYTICS_HOST
---
## 3. 배포 전 사전 확인
### 3.1 Azure 로그인 상태 확인
```bash
az account show
```
**확인 사항**: 올바른 Azure 구독에 로그인되어 있는지 확인
### 3.2 AKS Credential 확인
```bash
kubectl cluster-info
```
**확인 사항**: aks-digitalgarage-01 클러스터에 연결되어 있는지 확인
AKS 자격 증명이 설정되어 있지 않다면 다음 명령 실행:
```bash
az aks get-credentials --resource-group <리소스그룹명> --name aks-digitalgarage-01
```
### 3.3 Namespace 존재 확인
```bash
kubectl get ns kt-event-marketing
```
**확인 사항**: kt-event-marketing 네임스페이스가 존재하는지 확인
네임스페이스가 없다면 생성:
```bash
kubectl create namespace kt-event-marketing
```
### 3.4 ImagePullSecret 확인
배포 전에 ACR 접근을 위한 ImagePullSecret이 네임스페이스에 존재하는지 확인:
```bash
kubectl get secret kt-event-marketing-frontend -n kt-event-marketing
```
ImagePullSecret이 없다면 생성:
```bash
kubectl create secret docker-registry kt-event-marketing-frontend \
--docker-server=acrdigitalgarage01.azurecr.io \
--docker-username=<ACR_USERNAME> \
--docker-password=<ACR_PASSWORD> \
--namespace=kt-event-marketing
```
---
## 4. 매니페스트 적용
### 4.1 모든 매니페스트 일괄 적용
```bash
kubectl apply -f deployment/k8s/
```
### 4.2 개별 매니페스트 적용 (선택 사항)
순서대로 적용하려면:
```bash
# 1. ConfigMap 적용
kubectl apply -f deployment/k8s/configmap.yaml
# 2. Service 적용
kubectl apply -f deployment/k8s/service.yaml
# 3. Deployment 적용
kubectl apply -f deployment/k8s/deployment.yaml
# 4. Ingress 적용
kubectl apply -f deployment/k8s/ingress.yaml
```
---
## 5. 배포 확인
### 5.1 ConfigMap 확인
```bash
kubectl get configmap cm-kt-event-marketing-frontend -n kt-event-marketing
```
ConfigMap 내용 상세 확인:
```bash
kubectl describe configmap cm-kt-event-marketing-frontend -n kt-event-marketing
```
### 5.2 Service 확인
```bash
kubectl get service kt-event-marketing-frontend -n kt-event-marketing
```
Service 상세 확인:
```bash
kubectl describe service kt-event-marketing-frontend -n kt-event-marketing
```
### 5.3 Deployment 확인
```bash
kubectl get deployment kt-event-marketing-frontend -n kt-event-marketing
```
Deployment 상세 확인:
```bash
kubectl describe deployment kt-event-marketing-frontend -n kt-event-marketing
```
### 5.4 Pod 상태 확인
```bash
kubectl get pods -n kt-event-marketing -l app=kt-event-marketing-frontend
```
Pod 상세 확인:
```bash
kubectl describe pod -n kt-event-marketing -l app=kt-event-marketing-frontend
```
Pod 로그 확인:
```bash
kubectl logs -n kt-event-marketing -l app=kt-event-marketing-frontend --tail=100
```
### 5.5 Ingress 확인
```bash
kubectl get ingress kt-event-marketing-frontend -n kt-event-marketing
```
Ingress 상세 확인:
```bash
kubectl describe ingress kt-event-marketing-frontend -n kt-event-marketing
```
---
## 6. 배포 검증
### 6.1 Health Check 확인
```bash
kubectl exec -n kt-event-marketing -it $(kubectl get pod -n kt-event-marketing -l app=kt-event-marketing-frontend -o jsonpath='{.items[0].metadata.name}') -- curl http://localhost:8080/health
```
### 6.2 웹 브라우저 접속 테스트
브라우저에서 다음 URL로 접속:
```
http://kt-event-marketing-frontend.20.214.196.128.nip.io
```
### 6.3 Runtime Config 확인
브라우저 개발자 도구 콘솔에서 확인:
```javascript
console.log(window.__runtime_config__);
```
모든 API 호스트가 `http://kt-event-marketing-api.20.214.196.128.nip.io`로 설정되어 있는지 확인
---
## 7. 트러블슈팅
### Pod가 Running 상태가 아닌 경우
#### ImagePullBackOff 에러
```bash
# ImagePullSecret 확인
kubectl get secret kt-event-marketing-frontend -n kt-event-marketing
# Secret이 없으면 생성
kubectl create secret docker-registry kt-event-marketing-frontend \
--docker-server=acrdigitalgarage01.azurecr.io \
--docker-username=<ACR_USERNAME> \
--docker-password=<ACR_PASSWORD> \
--namespace=kt-event-marketing
```
#### CrashLoopBackOff 에러
```bash
# Pod 로그 확인
kubectl logs -n kt-event-marketing -l app=kt-event-marketing-frontend --tail=100
# 이전 컨테이너 로그 확인
kubectl logs -n kt-event-marketing -l app=kt-event-marketing-frontend --previous
```
#### Probe Failure
```bash
# Pod 이벤트 확인
kubectl describe pod -n kt-event-marketing -l app=kt-event-marketing-frontend
# Health endpoint 직접 확인
kubectl exec -n kt-event-marketing -it $(kubectl get pod -n kt-event-marketing -l app=kt-event-marketing-frontend -o jsonpath='{.items[0].metadata.name}') -- curl http://localhost:8080/health
```
### Ingress로 접속이 안 되는 경우
#### Ingress Controller 확인
```bash
kubectl get svc ingress-nginx-controller -n ingress-nginx
```
#### Ingress 상태 확인
```bash
kubectl describe ingress kt-event-marketing-frontend -n kt-event-marketing
```
#### Service Endpoint 확인
```bash
kubectl get endpoints kt-event-marketing-frontend -n kt-event-marketing
```
### ConfigMap이 마운트되지 않는 경우
```bash
# Pod 내부 확인
kubectl exec -n kt-event-marketing -it $(kubectl get pod -n kt-event-marketing -l app=kt-event-marketing-frontend -o jsonpath='{.items[0].metadata.name}') -- ls -la /usr/share/nginx/html/runtime-env.js
# ConfigMap 마운트 확인
kubectl exec -n kt-event-marketing -it $(kubectl get pod -n kt-event-marketing -l app=kt-event-marketing-frontend -o jsonpath='{.items[0].metadata.name}') -- cat /usr/share/nginx/html/runtime-env.js
```
---
## 8. 재배포 방법
### 8.1 ConfigMap 업데이트 후 재배포
```bash
# ConfigMap 수정
kubectl edit configmap cm-kt-event-marketing-frontend -n kt-event-marketing
# Pod 재시작
kubectl rollout restart deployment kt-event-marketing-frontend -n kt-event-marketing
```
### 8.2 새 이미지 버전 배포
```bash
# 새 이미지로 업데이트
kubectl set image deployment/kt-event-marketing-frontend \
kt-event-marketing-frontend=acrdigitalgarage01.azurecr.io/kt-event-marketing-frontend/kt-event-marketing-frontend:새버전 \
-n kt-event-marketing
# 롤아웃 상태 확인
kubectl rollout status deployment/kt-event-marketing-frontend -n kt-event-marketing
```
### 8.3 매니페스트 파일 수정 후 재배포
```bash
# 매니페스트 적용
kubectl apply -f deployment/k8s/
# 롤아웃 상태 확인
kubectl rollout status deployment/kt-event-marketing-frontend -n kt-event-marketing
```
### 8.4 롤백
```bash
# 이전 버전으로 롤백
kubectl rollout undo deployment/kt-event-marketing-frontend -n kt-event-marketing
# 특정 리비전으로 롤백
kubectl rollout undo deployment/kt-event-marketing-frontend --to-revision=<리비전번호> -n kt-event-marketing
# 롤아웃 히스토리 확인
kubectl rollout history deployment/kt-event-marketing-frontend -n kt-event-marketing
```
---
## 9. 삭제 방법
### 9.1 전체 삭제
```bash
kubectl delete -f deployment/k8s/
```
### 9.2 개별 리소스 삭제
```bash
# Ingress 삭제
kubectl delete ingress kt-event-marketing-frontend -n kt-event-marketing
# Deployment 삭제
kubectl delete deployment kt-event-marketing-frontend -n kt-event-marketing
# Service 삭제
kubectl delete service kt-event-marketing-frontend -n kt-event-marketing
# ConfigMap 삭제
kubectl delete configmap cm-kt-event-marketing-frontend -n kt-event-marketing
```
---
## 10. 모니터링
### 10.1 실시간 Pod 로그 확인
```bash
kubectl logs -n kt-event-marketing -l app=kt-event-marketing-frontend -f
```
### 10.2 리소스 사용량 확인
```bash
kubectl top pod -n kt-event-marketing -l app=kt-event-marketing-frontend
```
### 10.3 이벤트 모니터링
```bash
kubectl get events -n kt-event-marketing --sort-by='.lastTimestamp'
```
---
## 부록: 생성된 매니페스트 파일
### A. ConfigMap (configmap.yaml)
- 파일 위치: `deployment/k8s/configmap.yaml`
- 용도: runtime-env.js 설정 주입
- 마운트 경로: `/usr/share/nginx/html/runtime-env.js`
### B. Service (service.yaml)
- 파일 위치: `deployment/k8s/service.yaml`
- 타입: ClusterIP
- 포트: 8080
### C. Deployment (deployment.yaml)
- 파일 위치: `deployment/k8s/deployment.yaml`
- 레플리카: 1
- 이미지: `acrdigitalgarage01.azurecr.io/kt-event-marketing-frontend/kt-event-marketing-frontend:latest`
- 리소스 제한: CPU 256m-1024m, Memory 256Mi-1024Mi
### D. Ingress (ingress.yaml)
- 파일 위치: `deployment/k8s/ingress.yaml`
- 호스트: `kt-event-marketing-frontend.20.214.196.128.nip.io`
- 백엔드: kt-event-marketing-frontend:8080
---
## 문의사항
배포 중 문제가 발생하면 위의 트러블슈팅 섹션을 참고하거나 관리자에게 문의하세요.

View File

@ -0,0 +1,59 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: kt-event-marketing-frontend
namespace: kt-event-marketing
spec:
replicas: 1
selector:
matchLabels:
app: kt-event-marketing-frontend
template:
metadata:
labels:
app: kt-event-marketing-frontend
spec:
imagePullSecrets:
- name: kt-event-marketing-frontend
containers:
- name: kt-event-marketing-frontend
image: acrdigitalgarage01.azurecr.io/kt-event-marketing/kt-event-marketing-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"
startupProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
volumeMounts:
- name: runtime-config
mountPath: /usr/share/nginx/html/runtime-env.js
subPath: runtime-env.js
volumes:
- name: runtime-config
configMap:
name: cm-kt-event-marketing-frontend

View File

@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kt-event-marketing-frontend
namespace: kt-event-marketing
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: kt-event-marketing-frontend.20.214.196.128.nip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kt-event-marketing-frontend
port:
number: 8080

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: kt-event-marketing-frontend
namespace: kt-event-marketing
spec:
type: ClusterIP
selector:
app: kt-event-marketing-frontend
ports:
- protocol: TCP
port: 8080
targetPort: 8080

View File

@ -2,6 +2,7 @@
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
swcMinify: true, swcMinify: true,
output: 'standalone',
compiler: { compiler: {
emotion: true, emotion: true,
}, },

View File

@ -0,0 +1,81 @@
'use client';
import { useState } from 'react';
import ContentPreviewStep from '../steps/ContentPreviewStep';
import { useRouter } from 'next/navigation';
/**
* Content Service
* ContentPreviewStep을 .
*/
export default function ContentTestPage() {
const router = useRouter();
const [result, setResult] = useState<any>(null);
// 테스트용 이벤트 데이터
const testEventId = 'test-event-' + Date.now();
const testEventTitle = 'Content API 테스트 이벤트';
const testEventDescription = '콘텐츠 생성 기능을 테스트합니다';
const handleNext = (imageStyle: string, images: any[]) => {
console.log('ContentPreview 완료:', { imageStyle, images });
setResult({ imageStyle, images });
alert(`콘텐츠 생성 완료!\n스타일: ${imageStyle}\n이미지 수: ${images.length}`);
};
const handleSkip = () => {
console.log('ContentPreview 건너뛰기');
alert('콘텐츠 생성을 건너뛰었습니다.');
};
const handleBack = () => {
router.push('/events/create');
};
return (
<div>
<ContentPreviewStep
eventId={testEventId}
eventTitle={testEventTitle}
eventDescription={testEventDescription}
onNext={handleNext}
onSkip={handleSkip}
onBack={handleBack}
/>
{/* 결과 표시 (디버깅용) */}
{result && (
<div style={{
position: 'fixed',
bottom: 20,
right: 20,
background: 'white',
border: '2px solid #4CAF50',
borderRadius: 8,
padding: 16,
maxWidth: 400,
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
zIndex: 9999
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#4CAF50' }}> </h4>
<p style={{ margin: '5px 0' }}>
<strong>:</strong> {result.imageStyle}
</p>
<p style={{ margin: '5px 0' }}>
<strong> :</strong> {result.images.length}
</p>
<pre style={{
background: '#f5f5f5',
padding: 10,
borderRadius: 4,
fontSize: 12,
overflow: 'auto',
maxHeight: 200
}}>
{JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function GET(
request: NextRequest,
context: { params: Promise<{ eventDraftId: string }> }
) {
try {
const { eventDraftId } = await context.params;
const { searchParams } = new URL(request.url);
const style = searchParams.get('style');
const platform = searchParams.get('platform');
// eventDraftId is now eventId in the API
let url = `${CONTENT_API_BASE_URL}/api/v1/content/events/${eventDraftId}/images`;
const queryParams = [];
if (style) queryParams.push(`style=${style}`);
if (platform) queryParams.push(`platform=${platform}`);
if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
}
console.log('🔄 Proxying images request to Content API:', { url });
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Content API error:', response.status, errorText);
return NextResponse.json(
{ error: 'Failed to get images', details: errorText },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('❌ Proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084'; const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,

View File

@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
console.log('🔄 Proxying image generation request to Content API:', {
url: `${CONTENT_API_BASE_URL}/api/v1/content/images/generate`,
body,
});
const response = await fetch(`${CONTENT_API_BASE_URL}/api/v1/content/images/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Content API error:', response.status, errorText);
return NextResponse.json(
{ error: 'Failed to generate images', details: errorText },
{ status: response.status }
);
}
const data = await response.json();
console.log('✅ Image generation job created:', data);
return NextResponse.json(data);
} catch (error) {
console.error('❌ Proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084'; const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {

View File

@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function GET(
request: NextRequest,
context: { params: Promise<{ jobId: string }> }
) {
try {
const { jobId } = await context.params;
console.log('🔄 Proxying job status request to Content API:', {
url: `${CONTENT_API_BASE_URL}/api/v1/content/images/jobs/${jobId}`,
});
const response = await fetch(`${CONTENT_API_BASE_URL}/api/v1/content/images/jobs/${jobId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Content API error:', response.status, errorText);
return NextResponse.json(
{ error: 'Failed to get job status', details: errorText },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('❌ Proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
const CONTENT_API_BASE_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL || 'http://localhost:8084'; const CONTENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,

View File

@ -12,7 +12,8 @@ export async function POST(request: NextRequest) {
const { objective } = body; const { objective } = body;
// 백엔드 API 호출 시도 // 백엔드 API 호출 시도
const backendUrl = 'http://localhost:8080/api/events/objectives'; const EVENT_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
const backendUrl = `${EVENT_HOST}/api/events/objectives`;
try { try {
const backendResponse = await fetch(backendUrl, { const backendResponse = await fetch(backendUrl, {

View File

@ -1,4 +1,5 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import Script from 'next/script';
import { MUIThemeProvider } from '@/shared/lib/theme-provider'; import { MUIThemeProvider } from '@/shared/lib/theme-provider';
import { ReactQueryProvider } from '@/shared/lib/react-query-provider'; import { ReactQueryProvider } from '@/shared/lib/react-query-provider';
import { AuthProvider } from '@/features/auth'; import { AuthProvider } from '@/features/auth';
@ -34,6 +35,7 @@ export default function RootLayout({
/> />
</head> </head>
<body> <body>
<Script src="/runtime-env.js" strategy="beforeInteractive" />
<MUIThemeProvider> <MUIThemeProvider>
<ReactQueryProvider> <ReactQueryProvider>
<AuthProvider> <AuthProvider>

View File

@ -1,7 +1,6 @@
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
const ANALYTICS_HOST = const ANALYTICS_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
process.env.NEXT_PUBLIC_ANALYTICS_HOST || 'http://localhost:8086';
export const analyticsClient: AxiosInstance = axios.create({ export const analyticsClient: AxiosInstance = axios.create({
baseURL: ANALYTICS_HOST, baseURL: ANALYTICS_HOST,

View File

@ -23,20 +23,10 @@ import type {
* *
* apiClient를 , baseURL을 . * apiClient를 , baseURL을 .
*/ */
const EVENT_API_BASE = '/api/v1/events';
const EVENT_HOST = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
/**
* Event Service용 API
* Event Service는 (8080)
*
* 환경: Next.js rewrites (CORS )
* 환경: 환경
*/
import axios from 'axios'; import axios from 'axios';
const isProduction = process.env.NODE_ENV === 'production'; const EVENT_API_BASE = '/api/v1/events';
const API_BASE_URL = isProduction ? EVENT_HOST : ''; // 개발 환경에서는 상대 경로 사용 const API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
const eventApiClient = axios.create({ const eventApiClient = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,

View File

@ -1,7 +1,7 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
// AI Service API 클라이언트 // AI Service API 클라이언트
const AI_API_BASE_URL = process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083'; const AI_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
export const aiApiClient: AxiosInstance = axios.create({ export const aiApiClient: AxiosInstance = axios.create({
baseURL: AI_API_BASE_URL, baseURL: AI_API_BASE_URL,

View File

@ -1,14 +1,15 @@
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
// 마이크로서비스별 호스트 설정 // 마이크로서비스별 호스트 설정
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
const API_HOSTS = { const API_HOSTS = {
user: process.env.NEXT_PUBLIC_USER_HOST || 'http://localhost:8081', user: GATEWAY_HOST,
event: process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080', event: GATEWAY_HOST,
content: process.env.NEXT_PUBLIC_CONTENT_HOST || 'http://localhost:8082', content: GATEWAY_HOST,
ai: process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083', ai: GATEWAY_HOST,
participation: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || 'http://localhost:8084', participation: GATEWAY_HOST,
distribution: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://localhost:8085', distribution: GATEWAY_HOST,
analytics: process.env.NEXT_PUBLIC_ANALYTICS_HOST || 'http://localhost:8086', analytics: GATEWAY_HOST,
}; };
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api'; const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';

View File

@ -1,13 +1,9 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
// Event Service API 클라이언트 // Event Service API 클라이언트
const EVENT_API_BASE_URL = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080'; const EVENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1'; const API_VERSION = 'v1';
const BASE_URL = `${EVENT_API_BASE_URL}/api/${API_VERSION}`;
// 개발 환경에서는 상대 경로 사용 (Next.js rewrites 프록시 또는 Mock API 사용)
// 프로덕션 환경에서는 환경 변수의 호스트 사용
const isProduction = process.env.NODE_ENV === 'production';
const BASE_URL = isProduction ? `${EVENT_API_BASE_URL}/api/${API_VERSION}` : `/api/${API_VERSION}`;
export const eventApiClient: AxiosInstance = axios.create({ export const eventApiClient: AxiosInstance = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,