Merge branch 'main' of https://github.com/hwanny1128/HGZero into chore/path

This commit is contained in:
cyjadela 2025-10-27 17:02:02 +09:00
commit 0be39486b4
230 changed files with 16994 additions and 20484 deletions

500
.github/actions-pipeline-guide.md vendored Normal file
View File

@ -0,0 +1,500 @@
# GitHub Actions CI/CD 파이프라인 구축 가이드
## 📋 목차
1. [개요](#개요)
2. [사전 준비사항](#사전-준비사항)
3. [GitHub 저장소 환경 구성](#github-저장소-환경-구성)
4. [디렉토리 구조](#디렉토리-구조)
5. [Kustomize 구조 설명](#kustomize-구조-설명)
6. [GitHub Actions 워크플로우](#github-actions-워크플로우)
7. [배포 방법](#배포-방법)
8. [롤백 방법](#롤백-방법)
9. [SonarQube 설정](#sonarqube-설정)
10. [트러블슈팅](#트러블슈팅)
---
## 개요
HGZero 프로젝트의 백엔드 서비스를 위한 GitHub Actions 기반 CI/CD 파이프라인입니다.
### 주요 기능
- ✅ Gradle 기반 빌드 및 테스트
- ✅ SonarQube 코드 품질 분석 (선택적)
- ✅ Azure Container Registry에 Docker 이미지 빌드 및 푸시
- ✅ Kustomize를 사용한 환경별(dev/staging/prod) 배포
- ✅ AKS 클러스터 자동 배포
### 지원 서비스
- **user**: 사용자 관리 서비스
- **meeting**: 회의 관리 서비스
- **stt**: 음성 인식 서비스
- **ai**: AI 처리 서비스
- **notification**: 알림 서비스
---
## 사전 준비사항
### 1. 프로젝트 정보
- **시스템명**: hgzero
- **ACR 이름**: acrdigitalgarage02
- **리소스 그룹**: rg-digitalgarage-02
- **AKS 클러스터**: aks-digitalgarage-02
- **네임스페이스**: hgzero
- **JDK 버전**: 21
### 2. 필수 도구
- Git
- kubectl
- Azure CLI
- Kustomize (자동 설치됨)
---
## GitHub 저장소 환경 구성
### 1. Repository Secrets 설정
`Repository Settings > Secrets and variables > Actions > Repository secrets`에 다음 항목을 등록하세요:
#### Azure 인증 정보
```json
AZURE_CREDENTIALS:
{
"clientId": "{클라이언트ID}",
"clientSecret": "{클라이언트시크릿}",
"subscriptionId": "{구독ID}",
"tenantId": "{테넌트ID}"
}
```
**예시:**
```json
{
"clientId": "5e4b5b41-7208-48b7-b821-d6d5acf50ecf",
"clientSecret": "ldu8Q~GQEzFYU.dJX7_QsahR7n7C2xqkIM6hqbV8",
"subscriptionId": "2513dd36-7978-48e3-9a7c-b221d4874f66",
"tenantId": "4f0a3bfd-1156-4cce-8dc2-a049a13dba23"
}
```
#### ACR Credentials
ACR Credential을 확인하려면:
```bash
az acr credential show --name acrdigitalgarage02
```
등록할 Secrets:
```
ACR_USERNAME: acrdigitalgarage02
ACR_PASSWORD: {ACR 패스워드}
```
#### SonarQube 설정
**SONAR_HOST_URL 확인:**
```bash
kubectl get svc -n sonarqube
```
출력된 External IP를 사용하여 `http://{External IP}` 형식으로 설정
**SONAR_TOKEN 생성:**
1. SonarQube에 로그인 (기본: admin/admin)
2. 우측 상단 'Administrator' > My Account 클릭
3. Security 탭 선택 후 토큰 생성
등록할 Secrets:
```
SONAR_TOKEN: {SonarQube 토큰}
SONAR_HOST_URL: http://{External IP}
```
#### Docker Hub (Rate Limit 해결용)
**패스워드 생성:**
1. [Docker Hub](https://hub.docker.com) 로그인
2. 우측 상단 프로필 아이콘 > Account Settings
3. 좌측 메뉴 'Personal Access Tokens' 클릭하여 생성
등록할 Secrets:
```
DOCKERHUB_USERNAME: {Docker Hub 사용자명}
DOCKERHUB_PASSWORD: {Docker Hub 패스워드 또는 토큰}
```
### 2. Repository Variables 설정
`Repository Settings > Secrets and variables > Actions > Variables > Repository variables`에 등록:
```
ENVIRONMENT: dev
SKIP_SONARQUBE: true
```
---
## 디렉토리 구조
```
.github/
├── workflows/
│ └── backend-cicd.yaml # GitHub Actions 워크플로우
├── kustomize/
│ ├── base/ # 기본 Kubernetes 매니페스트
│ │ ├── common/ # 공통 리소스
│ │ │ ├── cm-common.yaml
│ │ │ ├── secret-common.yaml
│ │ │ ├── secret-imagepull.yaml
│ │ │ └── ingress.yaml
│ │ ├── user/ # User 서비스
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── secret-user.yaml
│ │ ├── meeting/ # Meeting 서비스
│ │ ├── stt/ # STT 서비스
│ │ ├── ai/ # AI 서비스
│ │ ├── notification/ # Notification 서비스
│ │ └── kustomization.yaml
│ └── overlays/ # 환경별 오버레이
│ ├── dev/ # 개발 환경
│ │ ├── kustomization.yaml
│ │ ├── cm-common-patch.yaml
│ │ ├── secret-common-patch.yaml
│ │ ├── ingress-patch.yaml
│ │ ├── deployment-{service}-patch.yaml
│ │ └── secret-{service}-patch.yaml
│ ├── staging/ # 스테이징 환경
│ └── prod/ # 프로덕션 환경
├── config/ # 환경별 설정
│ ├── deploy_env_vars_dev
│ ├── deploy_env_vars_staging
│ └── deploy_env_vars_prod
└── scripts/
└── deploy-actions.sh # 수동 배포 스크립트
```
---
## Kustomize 구조 설명
### Base 구조
Base는 모든 환경에서 공통으로 사용되는 기본 매니페스트입니다.
**주요 리소스:**
- **ConfigMap (cm-common)**: 환경 변수, 프로파일 설정
- **Secret (secret-common)**: JWT 시크릿, Redis 패스워드
- **Ingress**: API 라우팅 규칙
- **Deployment**: 각 서비스별 배포 설정
- **Service**: 각 서비스별 ClusterIP 서비스
- **Secret**: 각 서비스별 데이터베이스 연결 정보
### Overlay 구조
각 환경(dev/staging/prod)별로 Base를 오버라이드합니다.
#### DEV 환경
- **Replicas**: 1
- **Resources**: CPU 256m-1024m, Memory 256Mi-1024Mi
- **Profile**: dev
- **DDL**: update
- **Log Level**: DEBUG
- **Image Tag**: dev-{timestamp}
#### STAGING 환경
- **Replicas**: 2
- **Resources**: CPU 512m-2048m, Memory 512Mi-2048Mi
- **Profile**: staging
- **DDL**: validate
- **Log Level**: INFO
- **Image Tag**: staging-{timestamp}
- **SSL**: Enabled
#### PROD 환경
- **Replicas**: 3
- **Resources**: CPU 1024m-4096m, Memory 1024Mi-4096Mi
- **Profile**: prod
- **DDL**: validate
- **Log Level**: WARN
- **JWT Expiration**: 짧게 설정 (보안 강화)
- **Image Tag**: prod-{timestamp}
- **SSL**: Enabled
---
## GitHub Actions 워크플로우
### 트리거 조건
1. **Push 이벤트**:
- 브랜치: `main`, `develop`
- 경로: 서비스 코드 변경 시 (`user/**`, `meeting/**` 등)
2. **Pull Request**:
- 대상 브랜치: `main`
3. **수동 실행 (workflow_dispatch)**:
- Environment 선택: dev/staging/prod
- SonarQube 분석 스킵 선택: true/false
### 워크플로우 단계
#### 1. Build Job
1. 소스코드 체크아웃
2. JDK 21 설정
3. 환경 결정 (input 또는 기본값 dev)
4. Gradle 빌드 (테스트 제외)
5. SonarQube 분석 (선택적)
6. 빌드 아티팩트 업로드
7. 이미지 태그 생성 (타임스탬프 기반)
#### 2. Release Job
1. 빌드 아티팩트 다운로드
2. Docker Buildx 설정
3. Docker Hub 로그인 (Rate Limit 방지)
4. ACR 로그인
5. 각 서비스별 Docker 이미지 빌드 및 푸시
#### 3. Deploy Job
1. Azure CLI 설치 및 로그인
2. kubectl 설정
3. AKS Credentials 가져오기
4. 네임스페이스 생성
5. Kustomize 설치
6. 이미지 태그 업데이트
7. 매니페스트 적용
8. Deployment Ready 대기
---
## 배포 방법
### 1. 자동 배포 (Push/PR)
코드를 `main` 또는 `develop` 브랜치에 push하면 자동으로 dev 환경에 배포됩니다.
```bash
git add .
git commit -m "feat: 새로운 기능 추가"
git push origin develop
```
### 2. 수동 배포 (GitHub Actions UI)
1. GitHub Repository > Actions 탭 이동
2. "Backend Services CI/CD" 워크플로우 선택
3. "Run workflow" 버튼 클릭
4. 환경 선택 (dev/staging/prod)
5. SonarQube 분석 스킵 여부 선택
6. "Run workflow" 실행
### 3. 로컬에서 수동 배포
```bash
# 스크립트 실행 권한 확인
chmod +x .github/scripts/deploy-actions.sh
# DEV 환경에 배포 (기본)
./.github/scripts/deploy-actions.sh dev latest
# STAGING 환경에 특정 이미지 태그로 배포
./.github/scripts/deploy-actions.sh staging 20250127120000
# PROD 환경에 배포
./.github/scripts/deploy-actions.sh prod 20250127120000
```
---
## 롤백 방법
### 1. GitHub Actions를 통한 롤백
1. GitHub > Actions > 성공한 이전 워크플로우 선택
2. "Re-run all jobs" 클릭
3. 이전 버전으로 재배포됨
### 2. kubectl을 이용한 롤백
```bash
# 특정 Revision으로 롤백
kubectl rollout undo deployment/user -n hgzero --to-revision=2
# 이전 버전으로 롤백
kubectl rollout undo deployment/user -n hgzero
# 롤백 상태 확인
kubectl rollout status deployment/user -n hgzero
# Rollout 히스토리 확인
kubectl rollout history deployment/user -n hgzero
```
### 3. 수동 스크립트를 이용한 롤백
```bash
# 이전 안정 버전의 이미지 태그로 배포
./.github/scripts/deploy-actions.sh dev 20250126110000
```
---
## SonarQube 설정
### 프로젝트 생성
각 서비스별로 SonarQube 프로젝트를 생성하세요:
- hgzero-user-dev
- hgzero-meeting-dev
- hgzero-stt-dev
- hgzero-ai-dev
- hgzero-notification-dev
### Quality Gate 설정
기본 Quality Gate 설정:
- **Coverage**: >= 80%
- **Duplicated Lines**: <= 3%
- **Maintainability Rating**: <= A
- **Reliability Rating**: <= A
- **Security Rating**: <= A
### Gradle 설정 (이미 구성됨)
```gradle
// build.gradle
plugins {
id 'org.sonarqube' version '4.0.0.2929'
id 'jacoco'
}
sonarqube {
properties {
property "sonar.projectKey", "hgzero-${project.name}"
property "sonar.projectName", "hgzero-${project.name}"
}
}
jacocoTestReport {
reports {
xml.enabled true
}
}
```
---
## 트러블슈팅
### 1. 이미지 Pull 실패
**증상**: `ImagePullBackOff` 또는 `ErrImagePull`
**해결 방법**:
```bash
# ACR 로그인 테스트
az acr login --name acrdigitalgarage02
# Image Pull Secret 재생성
kubectl delete secret acr-secret -n hgzero
kubectl create secret docker-registry acr-secret \
--docker-server=acrdigitalgarage02.azurecr.io \
--docker-username=acrdigitalgarage02 \
--docker-password={ACR_PASSWORD} \
-n hgzero
```
### 2. Deployment 타임아웃
**증상**: `deployment "user" exceeded its progress deadline`
**해결 방법**:
```bash
# Pod 상태 확인
kubectl get pods -n hgzero
# Pod 로그 확인
kubectl logs -n hgzero {pod-name}
# Events 확인
kubectl describe pod -n hgzero {pod-name}
```
### 3. Health Check 실패
**증상**: Deployment가 Ready 상태로 전환되지 않음
**해결 방법**:
```bash
# Actuator health 엔드포인트 확인
kubectl exec -n hgzero {pod-name} -- curl http://localhost:8080/actuator/health
# 애플리케이션 로그 확인
kubectl logs -n hgzero {pod-name} -f
```
### 4. Kustomize 빌드 오류
**증상**: `kustomize build` 실패
**해결 방법**:
```bash
# 로컬에서 Kustomize 검증
kubectl kustomize .github/kustomize/overlays/dev
# YAML 문법 검증
yamllint .github/kustomize/overlays/dev/*.yaml
```
### 5. SonarQube 연결 실패
**증상**: SonarQube Analysis 단계에서 연결 오류
**해결 방법**:
1. SONAR_HOST_URL이 올바른지 확인
2. SONAR_TOKEN이 유효한지 확인
3. SonarQube 서비스 상태 확인:
```bash
kubectl get pods -n sonarqube
kubectl logs -n sonarqube {sonarqube-pod}
```
### 6. 환경 변수 로드 실패
**증상**: 환경 설정 파일을 찾을 수 없음
**해결 방법**:
```bash
# 파일 존재 확인
ls -la .github/config/
# 파일 내용 확인
cat .github/config/deploy_env_vars_dev
```
---
## 참고 자료
- [Kustomize 공식 문서](https://kustomize.io/)
- [GitHub Actions 문서](https://docs.github.com/en/actions)
- [Azure Container Registry 문서](https://docs.microsoft.com/en-us/azure/container-registry/)
- [AKS 문서](https://docs.microsoft.com/en-us/azure/aks/)
- [SonarQube 문서](https://docs.sonarqube.org/)
---
## 문의 및 지원
문제가 발생하거나 질문이 있으시면:
1. GitHub Issues에 등록
2. DevOps 팀에 문의 (송주영)
3. Slack #devops 채널
---
**작성일**: 2025-01-27
**작성자**: DevOps Team (주영)
**버전**: 1.0.0

3
.github/config/deploy_env_vars_dev vendored Normal file
View File

@ -0,0 +1,3 @@
# dev Environment Configuration
resource_group=rg-digitalgarage-02
cluster_name=aks-digitalgarage-02

3
.github/config/deploy_env_vars_prod vendored Normal file
View File

@ -0,0 +1,3 @@
# prod Environment Configuration
resource_group=rg-digitalgarage-02
cluster_name=aks-digitalgarage-02

View File

@ -0,0 +1,3 @@
# staging Environment Configuration
resource_group=rg-digitalgarage-02
cluster_name=aks-digitalgarage-02

View File

@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai
labels:
app: ai
spec:
replicas: 1
selector:
matchLabels:
app: ai
template:
metadata:
labels:
app: ai
spec:
containers:
- name: ai
image: acrdigitalgarage02.azurecr.io/hgzero/ai:latest
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: cm-common
- secretRef:
name: secret-common
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: secret-ai
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: secret-ai
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: secret-ai
key: DB_PASSWORD
resources:
requests:
cpu: 256m
memory: 256Mi
limits:
cpu: 1024m
memory: 1024Mi
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-ai
labels:
app: ai
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service:5432/aidb"
DB_USERNAME: "aiuser"
DB_PASSWORD: "aipass123"

15
.github/kustomize/base/ai/service.yaml vendored Normal file
View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: ai
labels:
app: ai
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: ai

View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-common
data:
# Spring Profiles
SPRING_PROFILES_ACTIVE: "dev"
# Database Configuration
DDL_AUTO: "update"
SHOW_SQL: "true"
# JWT Configuration
JWT_ACCESS_TOKEN_EXPIRATION: "3600000" # 1 hour
JWT_REFRESH_TOKEN_EXPIRATION: "86400000" # 24 hours
# Logging Configuration
LOG_LEVEL: "INFO"
# Application Configuration
SERVER_PORT: "8080"
# Redis Configuration
REDIS_HOST: "redis-service"
REDIS_PORT: "6379"

View File

@ -0,0 +1,48 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hgzero
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- host: hgzero-api.20.214.196.128.nip.io
http:
paths:
- path: /api/users
pathType: Prefix
backend:
service:
name: user
port:
number: 8080
- path: /api/meetings
pathType: Prefix
backend:
service:
name: meeting
port:
number: 8080
- path: /api/stt
pathType: Prefix
backend:
service:
name: stt
port:
number: 8080
- path: /api/ai
pathType: Prefix
backend:
service:
name: ai
port:
number: 8080
- path: /api/notifications
pathType: Prefix
backend:
service:
name: notification
port:
number: 8080

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-common
type: Opaque
stringData:
# JWT Secret Key (Base64 encoded in production)
JWT_SECRET_KEY: "hgzero-jwt-secret-key-change-in-production"
# Redis Password
REDIS_PASSWORD: "redis-password-change-in-production"

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: acr-secret
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: e30K

View File

@ -0,0 +1,49 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: hgzero-base
resources:
# Common resources
- common/cm-common.yaml
- common/secret-common.yaml
- common/secret-imagepull.yaml
- common/ingress.yaml
# User service
- user/deployment.yaml
- user/service.yaml
- user/secret-user.yaml
# Meeting service
- meeting/deployment.yaml
- meeting/service.yaml
- meeting/secret-meeting.yaml
# STT service
- stt/deployment.yaml
- stt/service.yaml
- stt/secret-stt.yaml
# AI service
- ai/deployment.yaml
- ai/service.yaml
- ai/secret-ai.yaml
# Notification service
- notification/deployment.yaml
- notification/service.yaml
- notification/secret-notification.yaml
images:
- name: acrdigitalgarage02.azurecr.io/hgzero/user
newTag: latest
- name: acrdigitalgarage02.azurecr.io/hgzero/meeting
newTag: latest
- name: acrdigitalgarage02.azurecr.io/hgzero/stt
newTag: latest
- name: acrdigitalgarage02.azurecr.io/hgzero/ai
newTag: latest
- name: acrdigitalgarage02.azurecr.io/hgzero/notification
newTag: latest

View File

@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: meeting
labels:
app: meeting
spec:
replicas: 1
selector:
matchLabels:
app: meeting
template:
metadata:
labels:
app: meeting
spec:
containers:
- name: meeting
image: acrdigitalgarage02.azurecr.io/hgzero/meeting:latest
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: cm-common
- secretRef:
name: secret-common
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: secret-meeting
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: secret-meeting
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: secret-meeting
key: DB_PASSWORD
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-meeting
type: Opaque
stringData:
# Meeting Service Database Configuration (Development)
DB_URL: "jdbc:postgresql://postgres-meeting:5432/meeting"
DB_USERNAME: "meeting_user"
DB_PASSWORD: "meeting_password"

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: meeting
labels:
app: meeting
spec:
type: ClusterIP
selector:
app: meeting
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http

View File

@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: notification
labels:
app: notification
system: hgzero
spec:
replicas: 1
selector:
matchLabels:
app: notification
template:
metadata:
labels:
app: notification
system: hgzero
spec:
containers:
- name: notification
image: acrdigitalgarage02.azurecr.io/hgzero/notification:latest
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: cm-common
- secretRef:
name: secret-common
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: secret-notification
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: secret-notification
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: secret-notification
key: DB_PASSWORD
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-notification
labels:
app: notification
system: hgzero
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service:5432/notification_db"
DB_USERNAME: "notification_user"
DB_PASSWORD: "notification_pass"

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: notification
labels:
app: notification
system: hgzero
spec:
type: ClusterIP
selector:
app: notification
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http

View File

@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: stt
labels:
app: stt
spec:
replicas: 1
selector:
matchLabels:
app: stt
template:
metadata:
labels:
app: stt
spec:
containers:
- name: stt
image: acrdigitalgarage02.azurecr.io/hgzero/stt:latest
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: cm-common
- secretRef:
name: secret-common
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: secret-stt
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: secret-stt
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: secret-stt
key: DB_PASSWORD
resources:
requests:
cpu: 256m
memory: 256Mi
limits:
cpu: 1024m
memory: 1024Mi
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-stt
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service:5432/sttdb"
DB_USERNAME: "sttuser"
DB_PASSWORD: "sttpass"

15
.github/kustomize/base/stt/service.yaml vendored Normal file
View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: stt
labels:
app: stt
spec:
type: ClusterIP
selector:
app: stt
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http

View File

@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: user
labels:
app: user
spec:
replicas: 1
selector:
matchLabels:
app: user
template:
metadata:
labels:
app: user
spec:
containers:
- name: user
image: acrdigitalgarage02.azurecr.io/hgzero/user:latest
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: cm-common
- secretRef:
name: secret-common
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: secret-user
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: secret-user
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: secret-user
key: DB_PASSWORD
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-user
labels:
app: user
type: Opaque
stringData:
DB_URL: "jdbc:mysql://mysql-user:3306/userdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"
DB_USERNAME: "user"
DB_PASSWORD: "user1234"

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: user
labels:
app: user
spec:
type: ClusterIP
selector:
app: user
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http

View File

@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-common
data:
# Spring Profiles
SPRING_PROFILES_ACTIVE: "dev"
# Database Configuration
DDL_AUTO: "update"
SHOW_SQL: "true"
# JWT Configuration
JWT_ACCESS_TOKEN_EXPIRATION: "3600000" # 1 hour
JWT_REFRESH_TOKEN_EXPIRATION: "86400000" # 24 hours
# Logging Configuration
LOG_LEVEL: "DEBUG"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai
spec:
replicas: 1
template:
spec:
containers:
- name: ai
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: meeting
spec:
replicas: 1
template:
spec:
containers:
- name: meeting
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: notification
spec:
replicas: 1
template:
spec:
containers:
- name: notification
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: stt
spec:
replicas: 1
template:
spec:
containers:
- name: stt
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: user
spec:
replicas: 1
template:
spec:
containers:
- name: user
resources:
requests:
cpu: "256m"
memory: "256Mi"
limits:
cpu: "1024m"
memory: "1024Mi"

View File

@ -0,0 +1,48 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hgzero
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- host: hgzero-api.20.214.196.128.nip.io
http:
paths:
- path: /api/users
pathType: Prefix
backend:
service:
name: user
port:
number: 8080
- path: /api/meetings
pathType: Prefix
backend:
service:
name: meeting
port:
number: 8080
- path: /api/stt
pathType: Prefix
backend:
service:
name: stt
port:
number: 8080
- path: /api/ai
pathType: Prefix
backend:
service:
name: ai
port:
number: 8080
- path: /api/notifications
pathType: Prefix
backend:
service:
name: notification
port:
number: 8080

View File

@ -0,0 +1,84 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: hgzero
resources:
- ../../base
patches:
# Common patches
- path: cm-common-patch.yaml
target:
kind: ConfigMap
name: cm-common
- path: secret-common-patch.yaml
target:
kind: Secret
name: secret-common
- path: ingress-patch.yaml
target:
kind: Ingress
name: hgzero
# User service patches
- path: deployment-user-patch.yaml
target:
kind: Deployment
name: user
- path: secret-user-patch.yaml
target:
kind: Secret
name: secret-user
# Meeting service patches
- path: deployment-meeting-patch.yaml
target:
kind: Deployment
name: meeting
- path: secret-meeting-patch.yaml
target:
kind: Secret
name: secret-meeting
# STT service patches
- path: deployment-stt-patch.yaml
target:
kind: Deployment
name: stt
- path: secret-stt-patch.yaml
target:
kind: Secret
name: secret-stt
# AI service patches
- path: deployment-ai-patch.yaml
target:
kind: Deployment
name: ai
- path: secret-ai-patch.yaml
target:
kind: Secret
name: secret-ai
# Notification service patches
- path: deployment-notification-patch.yaml
target:
kind: Deployment
name: notification
- path: secret-notification-patch.yaml
target:
kind: Secret
name: secret-notification
images:
- name: acrdigitalgarage02.azurecr.io/hgzero/user
newTag: dev-latest
- name: acrdigitalgarage02.azurecr.io/hgzero/meeting
newTag: dev-latest
- name: acrdigitalgarage02.azurecr.io/hgzero/stt
newTag: dev-latest
- name: acrdigitalgarage02.azurecr.io/hgzero/ai
newTag: dev-latest
- name: acrdigitalgarage02.azurecr.io/hgzero/notification
newTag: dev-latest

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-ai
labels:
app: ai
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service-dev:5432/aidb_dev"
DB_USERNAME: "aiuser_dev"
DB_PASSWORD: "aipass_dev123"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-common
type: Opaque
stringData:
# JWT Secret Key (개발용)
JWT_SECRET_KEY: "hgzero-jwt-secret-key-change-in-production"
# Redis Password (개발용)
REDIS_PASSWORD: "redis-password-change-in-production"

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-meeting
type: Opaque
stringData:
# Meeting Service Database Configuration (Development)
DB_URL: "jdbc:postgresql://postgres-meeting-dev:5432/meeting_dev"
DB_USERNAME: "meeting_dev_user"
DB_PASSWORD: "meeting_dev_password"

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-notification
labels:
app: notification
system: hgzero
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service-dev:5432/notification_db_dev"
DB_USERNAME: "notification_dev_user"
DB_PASSWORD: "notification_dev_pass"

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-stt
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-service-dev:5432/sttdb_dev"
DB_USERNAME: "sttuser_dev"
DB_PASSWORD: "sttpass_dev"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: secret-user
labels:
app: user
type: Opaque
stringData:
DB_URL: "jdbc:mysql://mysql-user-dev:3306/userdb_dev?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"
DB_USERNAME: "user_dev"
DB_PASSWORD: "user_dev1234"

View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: common-config
namespace: hgzero
data:
SPRING_PROFILES_ACTIVE: "prod"
DDL_AUTO: "validate"
LOG_LEVEL: "WARN"
JWT_ACCESS_TOKEN_EXPIRATION: "1800000"
JWT_REFRESH_TOKEN_EXPIRATION: "43200000"
REDIS_HOST: "redis-svc.hgzero.svc.cluster.local"
REDIS_PORT: "6379"
KAFKA_BOOTSTRAP_SERVERS: "kafka-svc.hgzero.svc.cluster.local:9092"

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-deploy
namespace: hgzero
spec:
replicas: 3
template:
spec:
containers:
- name: ai-container
resources:
requests:
cpu: "1024m"
memory: "1024Mi"
limits:
cpu: "4096m"
memory: "4096Mi"

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: meeting-deploy
namespace: hgzero
spec:
replicas: 3
template:
spec:
containers:
- name: meeting-container
resources:
requests:
cpu: "1024m"
memory: "1024Mi"
limits:
cpu: "4096m"
memory: "4096Mi"

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: notification-deploy
namespace: hgzero
spec:
replicas: 3
template:
spec:
containers:
- name: notification-container
resources:
requests:
cpu: "1024m"
memory: "1024Mi"
limits:
cpu: "4096m"
memory: "4096Mi"

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: stt-deploy
namespace: hgzero
spec:
replicas: 3
template:
spec:
containers:
- name: stt-container
resources:
requests:
cpu: "1024m"
memory: "1024Mi"
limits:
cpu: "4096m"
memory: "4096Mi"

View File

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-deploy
namespace: hgzero
spec:
replicas: 3
template:
spec:
containers:
- name: user-container
resources:
requests:
cpu: "1024m"
memory: "1024Mi"
limits:
cpu: "4096m"
memory: "4096Mi"

View File

@ -0,0 +1,49 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hgzero-ingress
namespace: hgzero
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
rules:
- host: hgzero-api.example.com
http:
paths:
- path: /user(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: user-svc
port:
number: 8080
- path: /meeting(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: meeting-svc
port:
number: 8081
- path: /stt(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: stt-svc
port:
number: 8082
- path: /ai(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: ai-svc
port:
number: 8083
- path: /notification(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: notification-svc
port:
number: 8084

View File

@ -0,0 +1,84 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: hgzero
bases:
- ../../base
patches:
# Common patches
- path: cm-common-patch.yaml
target:
kind: ConfigMap
name: common-config
- path: secret-common-patch.yaml
target:
kind: Secret
name: common-secret
- path: ingress-patch.yaml
target:
kind: Ingress
name: hgzero-ingress
# User service patches
- path: deployment-user-patch.yaml
target:
kind: Deployment
name: user-deploy
- path: secret-user-patch.yaml
target:
kind: Secret
name: user-secret
# Meeting service patches
- path: deployment-meeting-patch.yaml
target:
kind: Deployment
name: meeting-deploy
- path: secret-meeting-patch.yaml
target:
kind: Secret
name: meeting-secret
# STT service patches
- path: deployment-stt-patch.yaml
target:
kind: Deployment
name: stt-deploy
- path: secret-stt-patch.yaml
target:
kind: Secret
name: stt-secret
# AI service patches
- path: deployment-ai-patch.yaml
target:
kind: Deployment
name: ai-deploy
- path: secret-ai-patch.yaml
target:
kind: Secret
name: ai-secret
# Notification service patches
- path: deployment-notification-patch.yaml
target:
kind: Deployment
name: notification-deploy
- path: secret-notification-patch.yaml
target:
kind: Secret
name: notification-secret
images:
- name: user-service
newTag: prod-latest
- name: meeting-service
newTag: prod-latest
- name: stt-service
newTag: prod-latest
- name: ai-service
newTag: prod-latest
- name: notification-service
newTag: prod-latest

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: ai-secret
namespace: hgzero
type: Opaque
stringData:
OPENAI_API_KEY: "your-openai-api-key"
LANGCHAIN_API_KEY: "your-langchain-api-key"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: common-secret
namespace: hgzero
type: Opaque
stringData:
JWT_SECRET_KEY: "your-prod-secret-key-change-this-in-production"
REDIS_PASSWORD: "your-prod-redis-password"
KAFKA_USERNAME: "admin"
KAFKA_PASSWORD: "admin-secret"

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: meeting-secret
namespace: hgzero
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-prod:5432/meeting_prod"
DB_USERNAME: "meeting_admin"
DB_PASSWORD: "meeting_prod_password"
OPENAI_API_KEY: "your-openai-api-key"
S3_ACCESS_KEY: "your-s3-access-key"
S3_SECRET_KEY: "your-s3-secret-key"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: notification-secret
namespace: hgzero
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-prod:5432/notification_prod"
DB_USERNAME: "notification_admin"
DB_PASSWORD: "notification_prod_password"
OPENAI_API_KEY: "your-openai-api-key"

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: stt-secret
namespace: hgzero
type: Opaque
stringData:
OPENAI_API_KEY: "your-openai-api-key"
S3_ACCESS_KEY: "your-s3-access-key"
S3_SECRET_KEY: "your-s3-secret-key"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: user-secret
namespace: hgzero
type: Opaque
stringData:
DB_URL: "jdbc:postgresql://postgres-prod:5432/user_prod"
DB_USERNAME: "user_admin"
DB_PASSWORD: "user_prod_password"
OPENAI_API_KEY: "your-openai-api-key"

View File

@ -0,0 +1,99 @@
# STAGING Environment Kustomize Overlay
## 개요
STAGING 환경을 위한 Kustomize overlay 설정입니다.
## 환경 설정
### 네임스페이스
- `hgzero`
### 공통 설정
- **프로파일**: staging
- **DDL 모드**: validate
- **로그 레벨**: INFO
- **Ingress 호스트**: hgzero-staging-api.example.com
- **SSL 리다이렉트**: 활성화
### 리소스 설정
- **Replicas**: 2
- **Resource Requests**:
- CPU: 512m
- Memory: 512Mi
- **Resource Limits**:
- CPU: 2048m
- Memory: 2048Mi
## 생성된 파일 목록 (총 14개)
### Common Patches (3개)
1. `cm-common-patch.yaml` - 공통 ConfigMap 패치
2. `secret-common-patch.yaml` - 공통 Secret 패치 (JWT, Redis)
3. `ingress-patch.yaml` - Ingress 패치 (호스트, SSL)
### Service-specific Patches (10개)
각 서비스(user, meeting, stt, ai, notification)별 2개 파일:
- `deployment-{서비스명}-patch.yaml` - Deployment 리소스 패치
- `secret-{서비스명}-patch.yaml` - DB 연결 정보 패치
4. `deployment-user-patch.yaml`
5. `secret-user-patch.yaml`
6. `deployment-meeting-patch.yaml`
7. `secret-meeting-patch.yaml`
8. `deployment-stt-patch.yaml`
9. `secret-stt-patch.yaml`
10. `deployment-ai-patch.yaml`
11. `secret-ai-patch.yaml`
12. `deployment-notification-patch.yaml`
13. `secret-notification-patch.yaml`
### Kustomization (1개)
14. `kustomization.yaml` - Kustomize 설정 파일
## 데이터베이스 설정
각 서비스별 STAGING 환경 DB 정보:
- **호스트**: {서비스명}-db-staging
- **포트**: 5432
- **데이터베이스명**: {서비스명}_db_staging
- **사용자명**: {서비스명}_service
- **비밀번호**: stringData로 정의 (실제 환경에서 변경 필요)
## 이미지 태그
모든 서비스: `staging-latest`
## 사용 방법
### 1. Kustomize 빌드 확인
```bash
kubectl kustomize .github/kustomize/overlays/staging
```
### 2. STAGING 환경 배포
```bash
kubectl apply -k .github/kustomize/overlays/staging
```
### 3. 배포 상태 확인
```bash
kubectl get all -n hgzero
```
### 4. Secret 업데이트 (실제 배포 시)
```bash
# Secret 파일들의 stringData를 실제 STAGING 환경 값으로 변경
vi .github/kustomize/overlays/staging/secret-common-patch.yaml
vi .github/kustomize/overlays/staging/secret-user-patch.yaml
# ... (각 서비스별 secret 파일 수정)
```
## 주의사항
1. Secret 파일들의 비밀번호는 반드시 실제 환경에 맞게 변경해야 합니다
2. Ingress 호스트명을 실제 STAGING 도메인으로 변경해야 합니다
3. DB 호스트명이 실제 STAGING 환경과 일치하는지 확인해야 합니다
4. 리소스 제한은 실제 부하 테스트 결과에 따라 조정이 필요할 수 있습니다
## 다음 단계
- PROD 환경 overlay 생성
- CI/CD 파이프라인과 통합
- Monitoring 및 Logging 설정 추가

View File

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: hgzero-common-config
data:
SPRING_PROFILES_ACTIVE: "staging"
DDL_AUTO: "validate"
LOG_LEVEL: "INFO"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-service
spec:
replicas: 2
template:
spec:
containers:
- name: ai-service
resources:
requests:
memory: "512Mi"
cpu: "512m"
limits:
memory: "2048Mi"
cpu: "2048m"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: meeting-service
spec:
replicas: 2
template:
spec:
containers:
- name: meeting-service
resources:
requests:
memory: "512Mi"
cpu: "512m"
limits:
memory: "2048Mi"
cpu: "2048m"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: notification-service
spec:
replicas: 2
template:
spec:
containers:
- name: notification-service
resources:
requests:
memory: "512Mi"
cpu: "512m"
limits:
memory: "2048Mi"
cpu: "2048m"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: stt-service
spec:
replicas: 2
template:
spec:
containers:
- name: stt-service
resources:
requests:
memory: "512Mi"
cpu: "512m"
limits:
memory: "2048Mi"
cpu: "2048m"

View File

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 2
template:
spec:
containers:
- name: user-service
resources:
requests:
memory: "512Mi"
cpu: "512m"
limits:
memory: "2048Mi"
cpu: "2048m"

View File

@ -0,0 +1,46 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hgzero-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: hgzero-staging-api.example.com
http:
paths:
- path: /api/users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 8080
- path: /api/meetings
pathType: Prefix
backend:
service:
name: meeting-service
port:
number: 8080
- path: /api/stt
pathType: Prefix
backend:
service:
name: stt-service
port:
number: 8080
- path: /api/ai
pathType: Prefix
backend:
service:
name: ai-service
port:
number: 8080
- path: /api/notifications
pathType: Prefix
backend:
service:
name: notification-service
port:
number: 8080

View File

@ -0,0 +1,91 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: hgzero
resources:
- ../../base
patches:
# Common patches
- path: cm-common-patch.yaml
target:
kind: ConfigMap
name: hgzero-common-config
- path: secret-common-patch.yaml
target:
kind: Secret
name: hgzero-common-secret
- path: ingress-patch.yaml
target:
kind: Ingress
name: hgzero-ingress
# User service patches
- path: deployment-user-patch.yaml
target:
kind: Deployment
name: user-service
- path: secret-user-patch.yaml
target:
kind: Secret
name: user-service-secret
# Meeting service patches
- path: deployment-meeting-patch.yaml
target:
kind: Deployment
name: meeting-service
- path: secret-meeting-patch.yaml
target:
kind: Secret
name: meeting-service-secret
# STT service patches
- path: deployment-stt-patch.yaml
target:
kind: Deployment
name: stt-service
- path: secret-stt-patch.yaml
target:
kind: Secret
name: stt-service-secret
# AI service patches
- path: deployment-ai-patch.yaml
target:
kind: Deployment
name: ai-service
- path: secret-ai-patch.yaml
target:
kind: Secret
name: ai-service-secret
# Notification service patches
- path: deployment-notification-patch.yaml
target:
kind: Deployment
name: notification-service
- path: secret-notification-patch.yaml
target:
kind: Secret
name: notification-service-secret
images:
- name: user-service
newTag: staging-latest
- name: meeting-service
newTag: staging-latest
- name: stt-service
newTag: staging-latest
- name: ai-service
newTag: staging-latest
- name: notification-service
newTag: staging-latest

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: ai-service-secret
type: Opaque
stringData:
DB_HOST: "ai-db-staging"
DB_PORT: "5432"
DB_NAME: "ai_db_staging"
DB_USERNAME: "ai_service"
DB_PASSWORD: "your-staging-ai-db-password"

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: hgzero-common-secret
type: Opaque
stringData:
JWT_SECRET: "your-staging-jwt-secret-key-here-change-in-production"
JWT_EXPIRATION: "3600000"
REDIS_PASSWORD: "your-staging-redis-password"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: meeting-service-secret
type: Opaque
stringData:
DB_HOST: "meeting-db-staging"
DB_PORT: "5432"
DB_NAME: "meeting_db_staging"
DB_USERNAME: "meeting_service"
DB_PASSWORD: "your-staging-meeting-db-password"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: notification-service-secret
type: Opaque
stringData:
DB_HOST: "notification-db-staging"
DB_PORT: "5432"
DB_NAME: "notification_db_staging"
DB_USERNAME: "notification_service"
DB_PASSWORD: "your-staging-notification-db-password"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: stt-service-secret
type: Opaque
stringData:
DB_HOST: "stt-db-staging"
DB_PORT: "5432"
DB_NAME: "stt_db_staging"
DB_USERNAME: "stt_service"
DB_PASSWORD: "your-staging-stt-db-password"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: user-service-secret
type: Opaque
stringData:
DB_HOST: "user-db-staging"
DB_PORT: "5432"
DB_NAME: "user_db_staging"
DB_USERNAME: "user_service"
DB_PASSWORD: "your-staging-user-db-password"

69
.github/scripts/deploy-actions.sh vendored Executable file
View File

@ -0,0 +1,69 @@
#!/bin/bash
set -e
ENVIRONMENT=${1:-dev}
IMAGE_TAG=${2:-latest}
echo "🚀 Manual deployment starting..."
echo "Environment: $ENVIRONMENT"
echo "Image Tag: $IMAGE_TAG"
# Check if kustomize is installed
if ! command -v kustomize &> /dev/null; then
echo "Installing Kustomize..."
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
fi
# Load environment variables from .github/config
if [[ -f ".github/config/deploy_env_vars_${ENVIRONMENT}" ]]; then
source ".github/config/deploy_env_vars_${ENVIRONMENT}"
echo "✅ Environment variables loaded for $ENVIRONMENT"
else
echo "❌ Environment configuration file not found: .github/config/deploy_env_vars_${ENVIRONMENT}"
exit 1
fi
# Create namespace
echo "📝 Creating namespace hgzero..."
kubectl create namespace hgzero --dry-run=client -o yaml | kubectl apply -f -
# 환경별 이미지 태그 업데이트 (.github/kustomize 사용)
cd .github/kustomize/overlays/${ENVIRONMENT}
echo "🔄 Updating image tags..."
# 서비스 배열 정의
services=(user meeting stt ai notification)
# 각 서비스별 이미지 태그 업데이트
for service in "${services[@]}"; do
kustomize edit set image acrdigitalgarage02.azurecr.io/hgzero/$service:${ENVIRONMENT}-${IMAGE_TAG}
done
echo "🚀 Deploying to Kubernetes..."
# 배포 실행
kubectl apply -k .
echo "⏳ Waiting for deployments to be ready..."
# 서비스별 배포 상태 확인
for service in "${services[@]}"; do
kubectl rollout status deployment/$service -n hgzero --timeout=300s || echo "⚠️ $service deployment timeout"
done
echo "🔍 Health check..."
# 각 서비스의 Health Check
for service in "${services[@]}"; do
POD=$(kubectl get pod -n hgzero -l app.kubernetes.io/name=$service -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$POD" ]]; then
kubectl -n hgzero exec $POD -- curl -f http://localhost:8080/actuator/health 2>/dev/null || echo "⚠️ $service health check failed"
else
echo "⚠️ $service pod not found"
fi
done
echo "📋 Service Information:"
kubectl get pods -n hgzero
kubectl get services -n hgzero
kubectl get ingress -n hgzero
echo "✅ GitHub Actions deployment completed successfully!"

276
.github/workflows/backend-cicd.yaml vendored Normal file
View File

@ -0,0 +1,276 @@
name: Backend Services CI/CD
on:
push:
branches: [ main, develop ]
paths:
- 'user/**'
- 'meeting/**'
- 'stt/**'
- 'ai/**'
- 'notification/**'
- 'common/**'
- '.github/**'
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
ENVIRONMENT:
description: 'Target environment'
required: true
default: 'dev'
type: choice
options:
- dev
- staging
- prod
SKIP_SONARQUBE:
description: 'Skip SonarQube Analysis'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
env:
REGISTRY: acrdigitalgarage02.azurecr.io
IMAGE_ORG: hgzero
RESOURCE_GROUP: rg-digitalgarage-02
AKS_CLUSTER: aks-digitalgarage-02
NAMESPACE: hgzero
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_outputs.outputs.image_tag }}
environment: ${{ steps.set_outputs.outputs.environment }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
cache: 'gradle'
- name: Determine environment
id: determine_env
run: |
# Use input parameter or default to 'dev'
ENVIRONMENT="${{ github.event.inputs.ENVIRONMENT || 'dev' }}"
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
- name: Load environment variables
id: env_vars
run: |
ENV=${{ steps.determine_env.outputs.environment }}
# Initialize variables with defaults
REGISTRY="acrdigitalgarage02.azurecr.io"
IMAGE_ORG="hgzero"
RESOURCE_GROUP="rg-digitalgarage-02"
AKS_CLUSTER="aks-digitalgarage-02"
NAMESPACE="hgzero"
# Read environment variables from .github/config file
if [[ -f ".github/config/deploy_env_vars_${ENV}" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
# Extract key-value pairs
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
# Override defaults if found in config
case "$key" in
"resource_group") RESOURCE_GROUP="$value" ;;
"cluster_name") AKS_CLUSTER="$value" ;;
esac
done < ".github/config/deploy_env_vars_${ENV}"
fi
# Export for other jobs
echo "REGISTRY=$REGISTRY" >> $GITHUB_ENV
echo "IMAGE_ORG=$IMAGE_ORG" >> $GITHUB_ENV
echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV
echo "AKS_CLUSTER=$AKS_CLUSTER" >> $GITHUB_ENV
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: |
./gradlew build -x test
- name: SonarQube Analysis & Quality Gate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: |
# Check if SonarQube should be skipped
SKIP_SONARQUBE="${{ github.event.inputs.SKIP_SONARQUBE || 'true' }}"
if [[ "$SKIP_SONARQUBE" == "true" ]]; then
echo "⏭️ Skipping SonarQube Analysis (SKIP_SONARQUBE=$SKIP_SONARQUBE)"
exit 0
fi
# Define services array
services=(user meeting stt ai notification)
# Run tests, coverage reports, and SonarQube analysis for each service
for service in "${services[@]}"; do
./gradlew :$service:test :$service:jacocoTestReport :$service:sonar \
-Dsonar.projectKey=hgzero-$service-${{ steps.determine_env.outputs.environment }} \
-Dsonar.projectName=hgzero-$service-${{ steps.determine_env.outputs.environment }} \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.java.binaries=build/classes/java/main \
-Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \
-Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/**
done
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: app-builds
path: |
user/build/libs/*.jar
meeting/build/libs/*.jar
stt/build/libs/*.jar
ai/build/libs/*.jar
notification/build/libs/*.jar
- name: Set outputs
id: set_outputs
run: |
# Generate timestamp for image tag
IMAGE_TAG=$(date +%Y%m%d%H%M%S)
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "environment=${{ steps.determine_env.outputs.environment }}" >> $GITHUB_OUTPUT
release:
name: Build and Push Docker Images
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: app-builds
- name: Set environment variables from build job
run: |
echo "REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV
echo "IMAGE_ORG=${{ env.IMAGE_ORG }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub (prevent rate limit)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Docker images for all services
run: |
# Define services array
services=(user meeting stt ai notification)
# Build and push each service image
for service in "${services[@]}"; do
echo "Building and pushing $service..."
docker build \
--build-arg BUILD_LIB_DIR="$service/build/libs" \
--build-arg ARTIFACTORY_FILE="$service.jar" \
-f deployment/container/Dockerfile-backend \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/$service:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} .
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/$service:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }}
done
deploy:
name: Deploy to Kubernetes
needs: [build, release]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set image tag environment variable
run: |
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
- name: Install Azure CLI
run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Setup kubectl
uses: azure/setup-kubectl@v3
- name: Get AKS Credentials
run: |
az aks get-credentials --resource-group ${{ env.RESOURCE_GROUP }} --name ${{ env.AKS_CLUSTER }} --overwrite-existing
- name: Create namespace
run: |
kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o yaml | kubectl apply -f -
- name: Install Kustomize
run: |
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
- name: Update Kustomize images and deploy
run: |
# 환경별 디렉토리로 이동
cd .github/kustomize/overlays/${{ env.ENVIRONMENT }}
# 각 서비스별 이미지 태그 업데이트
kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/user:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/meeting:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/stt:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/ai:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
kustomize edit set image ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/notification:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
# 매니페스트 적용
kubectl apply -k .
- name: Wait for deployments to be ready
run: |
echo "Waiting for deployments to be ready..."
kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/user --timeout=300s || true
kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/meeting --timeout=300s || true
kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/stt --timeout=300s || true
kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/ai --timeout=300s || true
kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/notification --timeout=300s || true

View File

@ -1,454 +0,0 @@
# 유저스토리 v2.2.0 → v2.3.0 변경사항 보고서
**작성일**: 2025-10-25
**작성자**: 지수 (Product Designer), 민준 (Product Owner)
**문서 버전**: 1.0
---
## 📋 개요
본 보고서는 AI기반 회의록 작성 및 이력 관리 개선 서비스의 유저스토리 문서가 v2.2.0에서 v2.3.0으로 업데이트되면서 변경된 내용과 그 의미를 분석합니다.
### 요약 통계
| 항목 | v2.2.0 | v2.3.0 | 변화 |
|------|--------|--------|------|
| **유저스토리 수** | 25개 | 27개 | +2개 (+8%) |
| **신규 추가** | - | 5개 | UFR-USER-010, UFR-USER-020, UFR-MEET-015, UFR-AI-030, UFR-NOTI-010 |
| **삭제/전환** | - | 2개 | AFR-USER-010, AFR-USER-020 → UFR로 전환 |
| **AFR 코드** | 2개 | 0개 | -2개 (100% 제거) |
| **UFR 코드** | 23개 | 27개 | +4개 (+17%) |
| **평균 상세도** | 20-30줄 | 60-100줄 | **약 3배 증가** |
| **프로토타입 연계** | 부분적 | 100% (10개 화면) | - |
| **표준 형식 적용** | 0% | 100% (27개) | - |
---
## 📊 한눈에 보는 변경사항
```
v2.2.0 (25개) v2.3.0 (27개)
┌─────────────────┐ ┌─────────────────┐
│ AFR-USER-010 │ ──────────────────>│ UFR-USER-010 ✨ │ (로그인 상세화)
│ AFR-USER-020 │ ──────────────────>│ UFR-USER-020 ✨ │ (대시보드 재설계)
├─────────────────┤ ├─────────────────┤
│ UFR-MEET-010 │ ──────────────────>│ UFR-MEET-010 ✨ │ (회의예약 개선)
│ │ │ UFR-MEET-015 🆕 │ (참석자 실시간 초대)
│ UFR-MEET-020 │ ──────────────────>│ UFR-MEET-020 ✨ │ (템플릿선택 상세화)
│ UFR-MEET-030 │ ──────────────────>│ UFR-MEET-030 ✨ │ (회의시작 4개 탭)
│ UFR-MEET-040 │ ──────────────────>│ UFR-MEET-040 ✨ │ (회의종료 3가지 액션)
│ UFR-MEET-050 │ ──────────────────>│ UFR-MEET-050 ✨ │ (최종확정 2가지 시나리오)
│ UFR-MEET-046 │ ──────────────────>│ UFR-MEET-046 ✨ │ (목록조회 샘플 30개)
│ UFR-MEET-047 │ ──────────────────>│ UFR-MEET-047 ✨ │ (상세조회 관련회의록)
│ UFR-MEET-055 │ ──────────────────>│ UFR-MEET-055 ✨ │ (회의록수정 3가지 시나리오)
├─────────────────┤ ├─────────────────┤
│ UFR-AI-010 │ ──────────────────>│ UFR-AI-010 │
│ UFR-AI-020 │ ──────────────────>│ UFR-AI-020 │
│ │ │ UFR-AI-030 🆕🎯 │ (실시간 AI 제안 - 차별화!)
│ UFR-AI-035 │ ──────────────────>│ UFR-AI-035 │
│ UFR-AI-036 │ ──────────────────>│ UFR-AI-036 │
│ UFR-AI-040 │ ──────────────────>│ UFR-AI-040 │
├─────────────────┤ ├─────────────────┤
│ UFR-STT-010 │ ──────────────────>│ UFR-STT-010 │
│ UFR-STT-020 │ ──────────────────>│ UFR-STT-020 │
├─────────────────┤ ├─────────────────┤
│ UFR-RAG-010 │ ──────────────────>│ UFR-RAG-010 │
│ UFR-RAG-020 │ ──────────────────>│ UFR-RAG-020 │
├─────────────────┤ ├─────────────────┤
│ UFR-COLLAB-010 │ ──────────────────>│ UFR-COLLAB-010 │
│ UFR-COLLAB-020 │ ──────────────────>│ UFR-COLLAB-020 │
│ UFR-COLLAB-030 │ ──────────────────>│ UFR-COLLAB-030 │
├─────────────────┤ ├─────────────────┤
│ UFR-TODO-010 │ ──────────────────>│ UFR-TODO-010 │
│ UFR-TODO-030 │ ──────────────────>│ UFR-TODO-030 │
│ UFR-TODO-040 │ ──────────────────>│ UFR-TODO-040 │
└─────────────────┘ ├─────────────────┤
│ UFR-NOTI-010 🆕 │ (알림발송 - 폴링 방식)
└─────────────────┘
범례:
🆕 = 완전 신규 추가
🎯 = 차별화 핵심 기능
✨ = 대폭 개선 (프로토타입 기반 재작성)
```
---
## 🎯 핵심 변경사항
### 1. 신규 추가된 유저스토리 (5개)
#### 1.1 UFR-USER-010: 로그인 🆕
- **이전**: AFR-USER-010 (간략한 인증 설명)
- **변경**: UFR-USER-010으로 전환 및 상세화
- **의미**:
- 로그인 프로세스 단계별 명시 (Enter 키 동작, 로딩 상태 등)
- 예외처리 시나리오 구체화 (사번 미입력, 비밀번호 8자 미만 등)
- 프로토타입 `01-로그인.html`과 1:1 매핑
#### 1.2 UFR-USER-020: 대시보드 🆕
- **이전**: AFR-USER-020 (간략한 대시보드 설명)
- **변경**: UFR-USER-020으로 전환 및 대폭 확장
- **의미**:
- 통계 블록, 최근 회의, 나의 Todo, 나의 회의록 위젯 상세 명세
- FAB 버튼 2가지 액션 (회의예약/바로 시작) 명확화
- 프로토타입 `02-대시보드.html`과 1:1 매핑
#### 1.3 UFR-MEET-015: 참석자 실시간 초대 🆕
- **이전**: 없음
- **변경**: 완전 신규 추가
- **의미**:
- 회의 진행 중 "참석자" 탭에서 실시간으로 참석자 추가 기능
- 검색 모달 → 추가 → WebSocket 동기화 → 알림 발송 흐름 명시
- **효과**: 회의 진행 중 동적 참석자 관리로 유연성 향상
- 프로토타입 `05-회의진행.html`의 "참석자" 탭과 연계
#### 1.4 UFR-AI-030: 실시간 AI 제안 🆕🎯
- **이전**: 없음
- **변경**: 완전 신규 추가
- **의미**:
- **차별화 전략 "지능형 회의 진행 지원" 실현**
- STT 텍스트 실시간 분석 → 주요 내용 감지 → AI 제안 카드 생성
- 제안 카드에서 메모 탭으로 드래그 앤 드롭으로 추가
- **효과**: 회의 중 놓치는 내용 최소화, 차별화 핵심 기능
- 프로토타입 `05-회의진행.html`의 "AI 제안" 탭과 연계
#### 1.5 UFR-NOTI-010: 알림 발송 🆕
- **이전**: 없음 (암묵적으로 Meeting Service에서 직접 발송)
- **변경**: Notification 서비스의 독립적인 유저스토리로 추가
- **의미**:
- **알림 아키텍처를 폴링 방식으로 통일**
- 1분 간격 폴링 → 이메일 발송 → 최대 3회 재시도
- 6가지 알림 유형 명시 (Todo 할당, Todo 완료, 회의 시작, 회의록 확정, 참석자 초대, 회의록 수정)
- **효과**: Notification 서비스 독립성 확보, 시스템 안정성 향상
---
### 2. 대폭 개선된 유저스토리 (주요 8개)
#### 2.1 UFR-MEET-010: 회의예약
- **변경사항**:
- 수행절차 10단계 명시 (FAB 버튼 → 입력 → 저장/완료)
- 입력 필드별 상세 명세 (타입, 필수 여부, 최대/최소값, UI 요소)
- 임시저장/예약 완료 2가지 시나리오 구분
- 예외처리 7가지 추가 (제목 미입력, 과거 날짜, 참석자 미선택 등)
- **의미**: 프로토타입 `03-회의예약.html` 기반 전면 재작성
#### 2.2 UFR-MEET-030: 회의시작
- **변경사항**:
- 회의 진행 화면 4개 탭 상세 명세 (녹음/메모, 참석자, AI 제안, 안건)
- 녹음 시작/일시정지/재시작 플로우 명시
- 참석자 상태 표시 (온라인/오프라인/참석중)
- 탭별 UI 요소와 인터랙션 상세화
- **의미**: 프로토타입 `05-회의진행.html` 4개 탭 구조 반영
#### 2.3 UFR-MEET-040: 회의종료
- **변경사항**:
- 회의 종료 후 3가지 액션 명시 (바로 확정, 나중에 확정, 검토 후 확정)
- 각 액션별 이동 화면 명확화
- 안건 요약 및 검증 상태 표시 추가
- **의미**: 프로토타입 `07-회의종료.html` 반영, 사용자 선택권 강화
#### 2.4 UFR-MEET-050: 최종확정
- **변경사항**:
- 2가지 시나리오 분리 (검토 후 확정, 회의 종료 화면에서 바로 확정)
- 안건별 검증 완료 여부 체크 로직 추가
- 미검증 안건 있을 시 확정 불가 정책 명시
- **의미**: 회의록 품질 보증 메커니즘 강화
#### 2.5 UFR-MEET-046: 회의록목록조회
- **변경사항**:
- 샘플 데이터 30개 명시 (제목, 날짜, 상태, 검증 현황 등)
- 필터/정렬 기능 상세화 (기간, 상태, 폴더별)
- 상태 배지 5종 추가 (진행중, 검토중, 확정완료 등)
- **의미**: 프로토타입 `12-회의록목록조회.html` 반영
#### 2.6 UFR-MEET-047: 회의록상세조회
- **변경사항**:
- 관련 회의록 섹션 추가 (AI가 자동 연결한 회의록 3개 표시)
- 안건별 검증 상태 표시 추가
- 용어 팝업 연계 (UFR-RAG-010) 명시
- **의미**: 프로토타입 `10-회의록상세조회.html` 반영, RAG 기능 연계
#### 2.7 UFR-MEET-055: 회의록수정
- **변경사항**:
- 3가지 진입 시나리오 명시 (회의종료 화면, 목록 화면, 상세조회 화면)
- 실시간 협업 플로우 상세화 (UFR-COLLAB-010, UFR-COLLAB-020 연계)
- 수정 저장/임시저장/취소 3가지 액션 구분
- **의미**: 프로토타입 `11-회의록수정.html` 반영, 협업 기능 강화
#### 2.8 UFR-COLLAB-020: 충돌해결
- **변경사항**:
- 안건 기반 충돌 방지 메커니즘 상세화
- 동일 안건 동시 수정 시 경고 표시 및 잠금 정책 명시
- 충돌 해결 시나리오 3가지 (대기, 새 안건 작성, 취소)
- **의미**: 실시간 협업 안정성 강화
---
### 3. 유지된 유저스토리 (14개)
다음 유저스토리들은 v2.2.0과 v2.3.0에서 ID와 핵심 내용이 유지되었습니다:
- UFR-AI-010 (회의록 자동 작성)
- UFR-AI-020 (Todo 자동 추출)
- UFR-AI-035 (섹션 AI 요약)
- UFR-AI-036 (AI 한줄 요약)
- UFR-AI-040 (관련 회의록 연결)
- UFR-STT-010 (음성 녹음 인식)
- UFR-STT-020 (텍스트 변환)
- UFR-RAG-010 (전문용어 감지)
- UFR-RAG-020 (맥락 기반 용어 설명)
- UFR-COLLAB-010 (회의록 수정 동기화)
- UFR-COLLAB-030 (검증 완료)
- UFR-TODO-010 (Todo 할당)
- UFR-TODO-030 (Todo 완료 처리)
- UFR-TODO-040 (Todo 관리)
---
## 📈 문서 품질 개선
### 3.1 유저스토리 형식 표준화
#### Before (v2.2.0) - 자유 형식
```
UFR-MEET-010: [회의예약] 회의 생성자로서 | 나는, ...
- 시나리오: 회의 예약 및 참석자 초대
회의 예약 화면에 접근한 상황에서 | ...
[입력 요구사항]
- 회의 제목: 최대 100자 (필수)
...
[처리 결과]
- 회의가 예약됨
...
- M/13
```
#### After (v2.3.0) - 표준 5단계 형식
```
### UFR-MEET-010: [회의예약] 회의 생성자로서 | 나는, ...
**수행절차:**
1. 대시보드에서 "회의예약" FAB 버튼 클릭
2. 회의 제목 입력 (최대 100자)
3. 날짜 선택 (오늘 이후 날짜, 달력 UI)
...
10. "임시저장" 버튼 또는 "예약 완료" 버튼 클릭
**입력:**
- 회의 제목: 텍스트 입력, 필수, 최대 100자, 문자 카운터 표시
- 날짜: date 타입, 필수, 오늘 이후 날짜만 선택 가능
...
**출력/결과:**
- 예약 완료: "회의가 예약되었습니다" 토스트 메시지, 대시보드로 이동
- 임시저장: "임시 저장되었습니다" 토스트 메시지
...
**예외처리:**
- 제목 미입력: "회의 제목을 입력해주세요" 토스트, 제목 필드 포커스
- 과거 날짜 선택: "과거 날짜는 선택할 수 없습니다" 토스트
...
**관련 유저스토리:**
- UFR-USER-020: 대시보드 조회
- UFR-MEET-020: 템플릿선택
```
### 3.2 개선 효과
| 섹션 | 개선 효과 |
|------|-----------|
| **수행절차** | 단계별 명확한 작업 흐름, 개발자가 UI 플로우 이해 가능 |
| **입력** | 필드 타입, 검증 규칙, UI 요소 상세 명세, API 명세서 작성 기준 제공 |
| **출력/결과** | 성공/실패 시나리오별 응답 명시, 테스트 케이스 작성 기준 제공 |
| **예외처리** | 에러 상황별 처리 방법 구체화, QA 시나리오 명확화 |
| **관련 유저스토리** | 기능 간 연계성 추적, 통합 테스트 범위 파악 용이 |
---
## 🏗️ 프로토타입 연계 강화
v2.3.0에서는 모든 유저스토리가 프로토타입 화면과 명확하게 연계되었습니다.
| 프로토타입 화면 | 연계 유저스토리 | 상태 |
|----------------|----------------|------|
| 01-로그인.html | UFR-USER-010 | ✅ 1:1 매핑 |
| 02-대시보드.html | UFR-USER-020 | ✅ 1:1 매핑 |
| 03-회의예약.html | UFR-MEET-010 | ✅ 1:1 매핑 |
| 04-템플릿선택.html | UFR-MEET-020 | ✅ 1:1 매핑 |
| 05-회의진행.html | UFR-MEET-030, UFR-MEET-015 (신규), UFR-AI-030 (신규) | ✅ 1:N 매핑 |
| 07-회의종료.html | UFR-MEET-040 | ✅ 1:1 매핑 |
| 10-회의록상세조회.html | UFR-MEET-047 | ✅ 1:1 매핑 |
| 11-회의록수정.html | UFR-MEET-055 | ✅ 1:1 매핑 |
| 12-회의록목록조회.html | UFR-MEET-046 | ✅ 1:1 매핑 |
| 08-최종확정.html | UFR-MEET-050 | ✅ 1:1 매핑 |
**결과**: 10개 프로토타입 화면 100% 유저스토리 연계 완료
---
## 🔑 핵심 아키텍처 변경
### 알림 아키텍처: 실시간 → 폴링 방식
#### Before (v2.2.0)
```
[Meeting Service] ──(실시간 발송)──> [Notification Service] ──> [Email]
Todo 할당 발생 → 즉시 이메일 발송
```
**문제점**:
- Meeting Service와 Notification Service 간 강한 결합
- 이메일 발송 실패 시 Meeting Service에 영향
#### After (v2.3.0)
```
[Meeting Service] ──(DB 레코드 생성)──> [Notification 테이블]
(1분 간격 폴링)
[Notification Service] ──> [Email]
(발송 상태 업데이트)
```
**개선 효과**:
- ✅ **Notification 서비스 독립성 강화**: 마이크로서비스 간 느슨한 결합
- ✅ **시스템 안정성 향상**: 이메일 발송 실패 시 자동 재시도 (최대 3회)
- ✅ **확장성 확보**: 폴링 주기 조정으로 트래픽 제어 가능
- ✅ **모니터링 용이**: 발송 대기/성공/실패 상태 DB에서 추적
---
## 💡 변경의 의미와 개선 효과
### 1. 사용자 경험 (UX) 개선
| 영역 | 개선 내용 | 효과 |
|------|----------|------|
| **회의 진행 중 유연성** | UFR-MEET-015 (참석자 실시간 초대) | 회의 중 동적 참석자 관리 가능 |
| **회의 중 놓침 방지** | UFR-AI-030 (실시간 AI 제안) 🎯 | 차별화 핵심 기능, 회의 중 주요 내용 실시간 감지 |
| **회의 종료 후 선택권** | UFR-MEET-040 (3가지 액션) | 바로 확정/나중에 확정/검토 후 확정 |
| **회의록 품질 보증** | UFR-MEET-050 (검증 후 확정) | 미검증 안건 있을 시 확정 불가 정책 |
| **실시간 협업 안정성** | UFR-COLLAB-020 (안건 기반 충돌 방지) | 동일 안건 동시 수정 시 경고 및 잠금 |
### 2. 기능적 개선
| 영역 | 개선 내용 | 효과 |
|------|----------|------|
| **알림 시스템 안정성** | UFR-NOTI-010 (폴링 방식) | Notification 서비스 독립성 확보, 재시도 메커니즘 |
| **차별화 전략 실현** | UFR-AI-030 (실시간 AI 제안) 🎯 | "지능형 회의 진행 지원" 구체화 |
| **프로토타입 정합성** | 10개 화면 100% 매핑 | 기획-디자인-개발 간 일관성 확보 |
| **유저스토리 표준화** | 5단계 표준 형식 | 개발 가이드 역할 강화, API 명세서 작성 기준 제공 |
### 3. 문서화 개선
| 영역 | 개선 내용 | 효과 |
|------|----------|------|
| **상세도 3배 증가** | 20-30줄 → 60-100줄 | 개발자가 구현에 필요한 모든 정보 확보 |
| **AFR 코드 폐지** | AFR → UFR 통일 | 유저스토리 체계 단순화 |
| **예외처리 명시** | 각 유저스토리별 5-7개 예외 시나리오 | QA 테스트 케이스 작성 기준 제공 |
| **관련 유저스토리 연계** | 기능 간 의존성 추적 | 통합 테스트 범위 명확화 |
---
## 📋 권장 후속 조치
### 🔴 긴급 (1주 내)
- [ ] **신규 유저스토리 3개 기반 API 설계**
- UFR-MEET-015: 참석자 실시간 초대 API
- UFR-AI-030: 실시간 AI 제안 API (SSE 또는 WebSocket)
- UFR-NOTI-010: 알림 폴링 및 발송 API
- [ ] **알림 아키텍처 폴링 방식 반영**
- 물리 아키텍처 다이어그램 업데이트
- Notification 테이블 스키마 정의
- 폴링 스케줄러 설계
- [ ] **프로토타입 ↔ 유저스토리 1:1 매핑 검증**
- 10개 화면별 유저스토리 매핑 검증
- 누락된 화면 또는 유저스토리 확인
### 🟡 중요 (2주 내)
- [ ] **API 설계서 v2.3.0 기반 전면 업데이트**
- 입력/출력 명세 반영 (타입, 필수 여부, 검증 규칙)
- 예외처리 시나리오 → HTTP 상태 코드 및 에러 메시지 매핑
- 관련 유저스토리 기반 API 그룹핑
- [ ] **예외처리 시나리오 → 테스트 케이스 전환**
- 각 유저스토리의 예외처리 섹션을 테스트 케이스로 변환
- 입력 검증 테스트 케이스 작성
- [ ] **관련 유저스토리 기반 통합 테스트 시나리오 작성**
- 예: UFR-MEET-010 → UFR-MEET-020 → UFR-MEET-030 전체 플로우 테스트
### 🟢 일반 (3주 내)
- [ ] **유저스토리별 개발 우선순위 재평가**
- 신규 유저스토리 3개 우선순위 결정
- 차별화 핵심 기능 (UFR-AI-030) 우선 개발 검토
- [ ] **신규 기능 3개 개발 일정 수립**
- UFR-MEET-015: 참석자 실시간 초대
- UFR-AI-030: 실시간 AI 제안 (Sprint 목표로 권장)
- UFR-NOTI-010: 알림 발송
- [ ] **프로토타입 기반 개발 가이드 작성**
- 프로토타입 → 유저스토리 → API → 컴포넌트 매핑 가이드
- 프론트엔드 개발자를 위한 프로토타입 활용 가이드
---
## 🔍 핵심 시사점 (Key Takeaways)
1. **v2.3.0은 프로토타입 분석을 통해 유저스토리를 전면 재정비한 버전**
- 10개 프로토타입 화면과 100% 매핑
- 실제 UI/UX 플로우를 유저스토리에 반영
2. **신규 기능 3개 추가로 차별화 강화**
- 특히 UFR-AI-030 (실시간 AI 제안)은 차별화 핵심 기능
3. **알림 아키텍처 폴링 방식으로 통일하여 시스템 안정성 확보**
- Notification 서비스 독립성 강화
- 재시도 메커니즘으로 안정성 향상
4. **유저스토리 형식 표준화로 개발 가이드 역할 강화**
- 5단계 표준 형식 (수행절차, 입력, 출력/결과, 예외처리, 관련 유저스토리)
- API 명세서 및 테스트 케이스 작성 기준 제공
5. **평균 유저스토리 상세도 약 3배 증가로 품질 대폭 향상**
- 개발자가 구현에 필요한 모든 정보 포함
- 예외처리, 검증 규칙, UI 요소까지 상세 명시
6. **기존 24개 유저스토리 ID 승계하여 연속성 유지**
- AFR-USER-010 → UFR-USER-010 전환
- 기존 설계 문서와의 연계성 유지
7. **프로토타입-유저스토리 1:1 매핑으로 개발 명확성 확보**
- 기획-디자인-개발 간 일관성 확보
- 개발 우선순위 및 Sprint 계획 수립 용이
---
## 📎 참고 자료
- **상세 분석 (JSON)**: `claude/userstory-comparison-v2.2.0-to-v2.3.0.json` (19KB)
- **상세 분석 (Markdown)**: `claude/userstory-comparison-v2.2.0-to-v2.3.0.md` (16KB)
- **요약 분석**: `claude/userstory-comparison-summary.md` (11KB)
- **유저스토리 v2.2.0 백업**: `design/userstory_v2.2.0_backup.md`
- **유저스토리 v2.3.0 현재**: `design/userstory.md`
---
**보고서 작성**: 지수 (Product Designer), 민준 (Product Owner)
**분석 일시**: 2025-10-25
**문서 버전**: 1.0

View File

@ -60,7 +60,7 @@
}
.meeting-title {
font-size: var(--font-large);
font-size: var(--font-h2);
font-weight: var(--font-weight-bold);
color: var(--gray-800);
margin: 0;
@ -100,7 +100,7 @@
}
.recording-time {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
}
@ -173,9 +173,9 @@
.info-row {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) 0;
align-items: flex-start;
gap: 0;
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--gray-300);
}
@ -186,14 +186,28 @@
.info-label {
font-weight: var(--font-weight-medium);
color: var(--gray-600);
font-size: var(--font-caption);
min-width: 60px;
font-size: var(--font-small);
width: 70px;
min-width: 70px;
max-width: 70px;
flex-shrink: 0;
padding-right: var(--space-sm);
border-right: 1px solid var(--gray-300);
margin-right: var(--space-sm);
}
/* 데스크톱에서 라벨 폭 확장 */
@media (min-width: 768px) {
.info-label {
width: 100px;
min-width: 100px;
max-width: 100px;
}
}
.info-value {
color: var(--gray-800);
font-size: var(--font-small);
font-size: var(--font-body);
line-height: 1.5;
flex: 1;
}
@ -229,7 +243,7 @@
background: transparent;
border: none;
cursor: pointer;
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-600);
transition: all var(--transition-fast);
@ -289,13 +303,13 @@
}
.participant-section-title {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-bold);
margin: 0;
}
.participant-count {
font-size: var(--font-caption);
font-size: var(--font-small);
color: var(--gray-600);
}
@ -307,13 +321,13 @@
.invite-input {
flex: 1;
font-size: var(--font-small);
font-size: var(--font-body);
padding: var(--space-sm);
}
.invite-btn {
padding: var(--space-sm) var(--space-md);
font-size: var(--font-small);
font-size: var(--font-body);
white-space: nowrap;
flex-shrink: 0;
}
@ -337,13 +351,13 @@
}
.participant-name {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-800);
}
.participant-email {
font-size: var(--font-caption);
font-size: var(--font-small);
color: var(--gray-500);
margin-top: 2px;
overflow: hidden;
@ -360,7 +374,7 @@
}
.memo-input-label {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-700);
margin-bottom: var(--space-xs);
@ -373,7 +387,7 @@
padding: var(--space-sm);
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
font-size: var(--font-small);
font-size: var(--font-body);
font-family: inherit;
resize: vertical;
line-height: 1.5;
@ -388,11 +402,11 @@
width: 100%;
margin-top: var(--space-xs);
padding: var(--space-sm);
font-size: var(--font-small);
font-size: var(--font-body);
}
.ai-suggestion-list-title {
font-size: var(--font-body);
font-size: var(--font-h3);
font-weight: var(--font-weight-bold);
color: var(--gray-900);
margin-bottom: var(--space-sm);
@ -416,7 +430,7 @@
}
.ai-suggestion-time {
font-size: var(--font-caption);
font-size: var(--font-small);
color: var(--gray-500);
font-weight: var(--font-weight-regular);
}
@ -446,7 +460,7 @@
}
.ai-suggestion-text {
font-size: var(--font-small);
font-size: var(--font-body);
color: var(--gray-700);
line-height: 1.5;
}
@ -460,13 +474,13 @@
.term-search-input {
flex: 1;
font-size: var(--font-small);
font-size: var(--font-body);
padding: var(--space-sm);
}
.term-search-btn {
padding: var(--space-sm) var(--space-md);
font-size: var(--font-small);
font-size: var(--font-body);
white-space: nowrap;
flex-shrink: 0;
}
@ -493,7 +507,7 @@
}
.term-name {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-bold);
color: var(--primary);
display: flex;
@ -512,14 +526,14 @@
}
.term-definition {
font-size: var(--font-small);
font-size: var(--font-body);
color: var(--gray-700);
line-height: 1.5;
margin-bottom: var(--space-xs);
}
.term-context {
font-size: 11px;
font-size: var(--font-small);
color: var(--gray-500);
padding-top: var(--space-xs);
border-top: 1px dashed #D0D0D0;
@ -546,7 +560,7 @@
}
.related-doc-title {
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-bold);
color: var(--primary);
margin-bottom: var(--space-xs);
@ -555,7 +569,7 @@
.related-doc-meta {
display: flex;
gap: var(--space-xs);
font-size: var(--font-caption);
font-size: var(--font-small);
color: var(--gray-600);
margin-bottom: var(--space-xs);
}
@ -566,7 +580,7 @@
}
.related-doc-text {
font-size: var(--font-small);
font-size: var(--font-body);
color: var(--gray-700);
line-height: 1.5;
}

File diff suppressed because it is too large Load Diff

View File

@ -107,11 +107,41 @@
margin-bottom: var(--space-md);
}
/* 회의 제목 컨테이너 */
.meeting-title-container {
display: flex;
flex-direction: column;
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
/* 배지 영역 (배지 + 크라운) */
.meeting-badges {
display: flex;
align-items: center;
gap: var(--space-xs);
}
/* 회의 제목 */
.meeting-basic-info h2 {
font-size: var(--font-h2);
font-weight: var(--font-weight-bold);
color: var(--gray-900);
margin-bottom: var(--space-sm);
margin: 0;
line-height: 1.3;
}
/* 데스크톱: 기존 가로 배치 유지 */
@media (min-width: 768px) {
.meeting-title-container {
flex-direction: row;
align-items: center;
gap: var(--space-sm);
}
.meeting-basic-info h2 {
margin: 0;
}
}
.info-row {
@ -165,23 +195,6 @@
.participant {
width: calc(50% - var(--space-md) / 2);
}
/* 통계 그리드: 모바일에서도 4열 유지, gap만 축소 */
.stats-grid {
gap: var(--space-xs);
}
.stat-item {
padding: var(--space-sm);
}
.stat-value {
font-size: var(--font-base);
}
.stat-label {
font-size: var(--font-xs);
}
}
/* 회의록 섹션 */
@ -279,22 +292,6 @@
margin-bottom: var(--space-md);
}
.reference-item {
background: var(--white);
border-radius: var(--radius-md);
padding: var(--space-sm);
margin-bottom: var(--space-sm);
cursor: pointer;
transition: all var(--transition-fast);
}
.reference-item:hover {
box-shadow: var(--shadow-sm);
}
.reference-item:last-child {
margin-bottom: 0;
}
.reference-header {
display: flex;
@ -310,7 +307,7 @@
.reference-title {
flex: 1;
font-size: var(--font-small);
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-900);
}
@ -338,7 +335,7 @@
}
.reference-meta {
font-size: var(--font-caption);
font-size: var(--font-small);
color: var(--gray-500);
margin-bottom: var(--space-xs);
}
@ -405,10 +402,11 @@
color: var(--white);
}
/* 통계 그리드 - 모바일 기본 (2x2) */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
grid-template-columns: repeat(2, 1fr);
gap: var(--space-sm);
margin-top: var(--space-md);
}
@ -431,6 +429,14 @@
color: var(--gray-500);
}
/* 데스크톱: 1x4 그리드 */
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
}
}
/* 결정사항 카드 */
.decision-card {
background: var(--white);
@ -462,7 +468,7 @@
border-top: 1px solid var(--gray-300);
}
/* Todo 진행상황 */
/* Todo 리스트 */
.todo-filters {
display: flex;
gap: var(--space-sm);
@ -523,7 +529,64 @@
margin-bottom: var(--space-xs);
}
/* Todo 진행상황 - 09-Todo관리 스타일 적용 */
/* Todo 리스트 - 단순 조회 스타일 */
.simple-todo-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.simple-todo-item {
position: relative;
padding: var(--space-md);
background: var(--white);
border-radius: var(--radius-md);
border: 1px solid var(--gray-300);
}
.simple-todo-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-sm);
margin-bottom: var(--space-xs);
}
.simple-todo-title {
flex: 1;
font-size: var(--font-body);
font-weight: var(--font-weight-medium);
color: var(--gray-900);
min-width: 0;
}
.simple-todo-edit-btn {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px;
color: var(--gray-500);
transition: all var(--transition-fast);
flex-shrink: 0;
border-radius: 4px;
}
.simple-todo-edit-btn:hover {
color: var(--primary);
background: var(--primary-light);
transform: scale(1.1);
}
.simple-todo-meta {
font-size: var(--font-small);
color: var(--gray-600);
display: flex;
gap: var(--space-md);
align-items: center;
}
/* Todo 리스트 - 09-Todo관리 스타일 적용 */
.todo-filters {
display: flex;
gap: var(--space-sm);
@ -688,6 +751,24 @@
display: block;
}
/* 대시보드 탭의 관련회의록 카드 스타일 강화 */
.card .reference-item {
border: 1px solid var(--gray-200) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
margin-bottom: var(--space-sm) !important;
}
.card .reference-item:hover {
border-color: var(--primary) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12) !important;
transform: translateY(-1px) !important;
}
.card .reference-item:active {
transform: translateY(0) !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08) !important;
}
/* 모바일 화면에서 관련회의록 왼쪽 정렬 */
@media (max-width: 600px) {
.reference-item {
@ -738,10 +819,14 @@
<!-- 기본 정보 카드 -->
<div class="info-card">
<div class="meeting-basic-info">
<div id="meeting-title-container" style="display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm);">
<span class="badge badge-complete">확정완료</span>
<!-- 생성자일 경우 👑 아이콘이 JavaScript로 추가됨 -->
<h2 style="margin: 0;">2025년 1분기 제품 기획 회의</h2>
<div class="meeting-title-container">
<!-- 배지 + 크라운 영역 -->
<div class="meeting-badges" id="meeting-badges">
<span class="badge badge-complete">확정완료</span>
<!-- 생성자일 경우 👑 아이콘이 JavaScript로 추가됨 -->
</div>
<!-- 회의 제목 -->
<h2>2025년 1분기 제품 기획 회의</h2>
</div>
<div class="info-row">
<span class="info-icon">📅</span>
@ -757,7 +842,7 @@
<div class="participant">
<div class="avatar avatar-green"></div>
<span class="participant-name">김민준</span>
<span class="role-badge">성자</span>
<span class="role-badge">성자</span>
</div>
<div class="participant">
<div class="avatar avatar-blue"></div>
@ -1044,60 +1129,65 @@
<!-- Todo 단순 조회 (MVP 스코프 축소 v1.5.1) -->
<div class="card mb-lg">
<h3 class="card-title">📋 Todo 진행상황</h3>
<h3 class="card-title">📋 Todo 리스트</h3>
<p style="font-size: var(--font-small); color: var(--gray-600); margin-bottom: var(--space-md);">
Todo 항목은 조회만 가능합니다. 제목, 담당자, 마감일 정보만 표시됩니다.
</p>
<!-- Todo 단순 조회 리스트 (제목 + 담당자 + 마감일만 표시) -->
<div style="display: flex; flex-direction: column; gap: var(--space-sm);">
<div style="padding: var(--space-md); background: var(--gray-50); border-radius: var(--radius-md); border: 1px solid var(--gray-200);">
<div style="font-weight: var(--font-weight-medium); color: var(--gray-900); margin-bottom: var(--space-xs);">
데이터베이스 스키마 설계
<!-- Todo 단순 조회 리스트 (제목 + 담당자 + 마감일 + 수정 버튼) -->
<div class="simple-todo-list">
<div class="simple-todo-item">
<div class="simple-todo-header">
<div class="simple-todo-title">데이터베이스 스키마 설계</div>
<button class="simple-todo-edit-btn" onclick="editTodo(1)" title="수정">✏️</button>
</div>
<div style="font-size: var(--font-small); color: var(--gray-600);">
<div class="simple-todo-meta">
<span>👤 이준호</span>
<span style="margin-left: var(--space-md);">📅 2025-10-20</span>
<span>📅 2025-10-20</span>
</div>
</div>
<div style="padding: var(--space-md); background: var(--gray-50); border-radius: var(--radius-md); border: 1px solid var(--gray-200);">
<div style="font-weight: var(--font-weight-medium); color: var(--gray-900); margin-bottom: var(--space-xs);">
API 명세서 작성
<div class="simple-todo-item">
<div class="simple-todo-header">
<div class="simple-todo-title">API 명세서 작성</div>
<button class="simple-todo-edit-btn" onclick="editTodo(2)" title="수정">✏️</button>
</div>
<div style="font-size: var(--font-small); color: var(--gray-600);">
<div class="simple-todo-meta">
<span>👤 이준호</span>
<span style="margin-left: var(--space-md);">📅 2025-10-23</span>
<span>📅 2025-10-23</span>
</div>
</div>
<div style="padding: var(--space-md); background: var(--gray-50); border-radius: var(--radius-md); border: 1px solid var(--gray-200);">
<div style="font-weight: var(--font-weight-medium); color: var(--gray-900); margin-bottom: var(--space-xs);">
예산 편성안 검토
<div class="simple-todo-item">
<div class="simple-todo-header">
<div class="simple-todo-title">예산 편성안 검토</div>
<button class="simple-todo-edit-btn" onclick="editTodo(3)" title="수정">✏️</button>
</div>
<div style="font-size: var(--font-small); color: var(--gray-600);">
<div class="simple-todo-meta">
<span>👤 김민준</span>
<span style="margin-left: var(--space-md);">📅 2025-10-22</span>
<span>📅 2025-10-22</span>
</div>
</div>
<div style="padding: var(--space-md); background: var(--gray-50); border-radius: var(--radius-md); border: 1px solid var(--gray-200);">
<div style="font-weight: var(--font-weight-medium); color: var(--gray-900); margin-bottom: var(--space-xs);">
UI 프로토타입 디자인
<div class="simple-todo-item">
<div class="simple-todo-header">
<div class="simple-todo-title">UI 프로토타입 디자인</div>
<button class="simple-todo-edit-btn" onclick="editTodo(4)" title="수정">✏️</button>
</div>
<div style="font-size: var(--font-small); color: var(--gray-600);">
<div class="simple-todo-meta">
<span>👤 최유진</span>
<span style="margin-left: var(--space-md);">📅 2025-10-28</span>
<span>📅 2025-10-28</span>
</div>
</div>
<div style="padding: var(--space-md); background: var(--gray-50); border-radius: var(--radius-md); border: 1px solid var(--gray-200);">
<div style="font-weight: var(--font-weight-medium); color: var(--gray-900); margin-bottom: var(--space-xs);">
사용자 피드백 분석
<div class="simple-todo-item">
<div class="simple-todo-header">
<div class="simple-todo-title">사용자 피드백 분석</div>
<button class="simple-todo-edit-btn" onclick="editTodo(5)" title="수정">✏️</button>
</div>
<div style="font-size: var(--font-small); color: var(--gray-600);">
<div class="simple-todo-meta">
<span>👤 김민준</span>
<span style="margin-left: var(--space-md);">📅 2025-10-19</span>
<span>📅 2025-10-19</span>
</div>
</div>
</div>
@ -1113,7 +1203,7 @@
<span class="reference-title">AI 기능 개선 회의</span>
<span class="relevance-badge relevance-high">92%</span>
</div>
<div class="reference-meta">2025-10-23 15:00 · 이준호</div>
<div class="reference-meta">이준호 · 2025-10-23 15:00</div>
<div class="reference-summary">
AI 요약 정확도 개선 방안 논의. BERT 모델 도입 및 학습 데이터 확보 계획 수립.
</div>
@ -1125,7 +1215,7 @@
<span class="reference-title">개발 리소스 계획 회의</span>
<span class="relevance-badge relevance-medium">88%</span>
</div>
<div class="reference-meta">2025-10-22 11:00 · 김민준</div>
<div class="reference-meta">김민준 · 2025-10-22 11:00</div>
<div class="reference-summary">
Q4 개발 리소스 현황 및 배분 계획. 신규 프로젝트 우선순위 협의.
</div>
@ -1137,7 +1227,7 @@
<span class="reference-title">경쟁사 분석 회의</span>
<span class="relevance-badge relevance-medium">78%</span>
</div>
<div class="reference-meta">2025-10-20 10:00 · 박서연</div>
<div class="reference-meta">박서연 · 2025-10-20 10:00</div>
<div class="reference-summary">
경쟁사 A, B, C 분석 결과. 우리의 차별점은 실시간 협업 및 검증 기능.
</div>
@ -1151,6 +1241,33 @@
<button class="btn btn-secondary" onclick="navigateTo('11-회의록수정.html')">수정</button>
</div>
<!-- Todo 추가 모달 -->
<div class="modal-overlay" id="addTodoModal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Todo 추가</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Todo 내용</label>
<input type="text" class="form-control" placeholder="할 일을 입력하세요">
</div>
<div class="form-group">
<label class="form-label">마감일</label>
<div class="date-input-wrapper">
<input type="date" class="form-control">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('addTodoModal')">취소</button>
<button class="btn btn-primary" onclick="addTodo()">추가</button>
</div>
</div>
</div>
<!-- Todo 편집 모달 -->
<div class="modal-overlay" id="editTodoModal">
<div class="modal">
@ -1159,6 +1276,7 @@
<button class="modal-close" onclick="closeModal('editTodoModal')">×</button>
</div>
<div class="modal-body">
<input type="hidden" id="editTodoId">
<div class="form-group">
<label class="form-label">Todo 제목 <span class="text-error">*</span></label>
<input type="text" id="editTodoTitle" class="form-control" placeholder="할 일을 입력하세요">
@ -1181,14 +1299,6 @@
</div>
<p class="form-hint">📅 마감일 변경 시 캘린더가 자동 업데이트됩니다</p>
</div>
<div class="form-group">
<label class="form-label">우선순위 <span class="text-error">*</span></label>
<select id="editTodoPriority" class="form-control">
<option value="high">높음</option>
<option value="medium">보통</option>
<option value="low">낮음</option>
</select>
</div>
<!-- 권한 안내 (동적 메시지) -->
<div class="alert alert-info" id="editTodoPermissionInfo">
<span class="material-icons" style="font-size: 20px;">info</span>
@ -1612,6 +1722,136 @@
if (filterCompletedCount) filterCompletedCount.textContent = completed;
}
/**
* 회의 생성자 여부 확인
* @param {string} meetingId - 회의 ID
* @param {string} userName - 사용자 이름
* @returns {boolean} 회의 생성자 여부
*/
function checkIfUserIsCreator(meetingId, userName) {
// 실제로는 서버 API를 호출하여 확인
// 프로토타입에서는 샘플 데이터로 시뮬레이션
const meetingCreators = {
'meeting-001': '김민준',
'meeting-002': '이서연',
'meeting-003': '박준호'
};
return meetingCreators[meetingId] === userName;
}
/**
* Todo 수정 함수 (index 기반)
* @param {number} index - Todo 인덱스 (1-based)
*/
function editTodo(index) {
// 1-based index를 0-based로 변환
const todo = meetingTodos[index - 1];
if (!todo) {
showToast('Todo를 찾을 수 없습니다', 'error');
return;
}
// 모달에 데이터 채우기
document.getElementById('editTodoId').value = index;
document.getElementById('editTodoTitle').value = todo.title;
// 담당자 처리 (assignee가 객체인 경우 name 속성 사용)
const assigneeName = typeof todo.assignee === 'object' ? todo.assignee.name : todo.assignee;
document.getElementById('editTodoAssignee').value = assigneeName;
document.getElementById('editTodoDueDate').value = todo.dueDate;
// 회의 생성자 여부 확인
const currentUser = '김민준'; // 현재 로그인 사용자
const isCreator = checkIfUserIsCreator(CURRENT_MEETING_ID, currentUser);
// 담당자 필드 표시 여부 결정
const assigneeGroup = document.getElementById('editTodoAssigneeGroup');
const permissionText = document.getElementById('editTodoPermissionText');
if (isCreator) {
// 회의 생성자: 담당자 변경 가능
assigneeGroup.style.display = 'block';
permissionText.textContent = '회의 생성자로서 모든 항목을 수정할 수 있습니다.';
} else {
// 일반 담당자: 담당자 변경 불가
assigneeGroup.style.display = 'none';
permissionText.textContent = '본인에게 할당된 Todo만 수정할 수 있습니다. 담당자는 변경할 수 없습니다.';
}
// 모달 열기
openModal('editTodoModal');
}
/**
* Todo 수정 저장 (index 기반)
*/
function saveTodoEdit() {
const index = parseInt(document.getElementById('editTodoId').value);
const title = document.getElementById('editTodoTitle').value.trim();
const assignee = document.getElementById('editTodoAssignee').value.trim();
const dueDate = document.getElementById('editTodoDueDate').value;
if (!title) {
showToast('제목을 입력해주세요', 'error');
return;
}
if (!assignee) {
showToast('담당자를 입력해주세요', 'error');
return;
}
if (!dueDate) {
showToast('마감일을 선택해주세요', 'error');
return;
}
// 1-based index를 0-based로 변환하여 Todo 업데이트
const todoIndex = index - 1;
if (todoIndex >= 0 && todoIndex < meetingTodos.length) {
const todo = meetingTodos[todoIndex];
const oldAssignee = typeof todo.assignee === 'object' ? todo.assignee.name : todo.assignee;
const oldDueDate = todo.dueDate;
// Todo 업데이트 (실제로는 API 호출)
todo.title = title;
// assignee가 객체인 경우 name 속성만 업데이트
if (typeof todo.assignee === 'object') {
todo.assignee.name = assignee;
} else {
todo.assignee = assignee;
}
todo.dueDate = dueDate;
showToast('Todo가 수정되었습니다', 'success');
// 담당자 변경 시 알림
if (oldAssignee !== assignee) {
setTimeout(() => {
showToast(`${oldAssignee}와 ${assignee}에게 알림이 전송되었습니다`, 'info');
}, 1000);
}
// 마감일 변경 시 알림
if (oldDueDate !== dueDate) {
setTimeout(() => {
showToast('캘린더가 업데이트되었습니다', 'info');
}, oldAssignee !== assignee ? 2000 : 1000);
}
}
closeModal('editTodoModal');
// 페이지 새로고침 (실제로는 해당 Todo 항목만 업데이트)
setTimeout(() => {
location.reload();
}, 1500);
}
/**
* 페이지 초기화
*/
@ -1621,13 +1861,13 @@
const isCreator = checkIfUserIsCreator(CURRENT_MEETING_ID, currentUser);
if (isCreator) {
const titleContainer = document.getElementById('meeting-title-container');
const badge = titleContainer.querySelector('.badge');
const badgesContainer = document.getElementById('meeting-badges');
const crownIcon = document.createElement('span');
crownIcon.textContent = '👑';
crownIcon.style.fontSize = '24px';
// badge 다음에 👑 삽입
badge.insertAdjacentElement('afterend', crownIcon);
crownIcon.style.fontSize = '20px';
crownIcon.title = '회의 생성자';
// 배지 영역에 크라운 추가
badgesContainer.appendChild(crownIcon);
}
updateTodoProgress();

View File

@ -174,6 +174,14 @@
.reference-item {
position: relative;
padding-right: 40px; /* 삭제 버튼 공간 확보 */
cursor: default; /* 카드 전체는 클릭 불가 */
}
/* 수정 페이지에서는 카드 hover 효과 제거 (삭제 버튼만 클릭 가능) */
.reference-item:hover {
transform: none;
border-color: var(--gray-200);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.remove-btn {
@ -399,7 +407,7 @@
<div class="ai-summary-edit">
<div class="ai-summary-header">
<span class="ai-summary-label">💡 AI 요약</span>
<button class="btn-primary btn-sm" onclick="regenerateSummary(1)">AI 재생성</button>
<button class="btn btn-primary btn-sm" onclick="regenerateSummary(1)">AI 재생성</button>
</div>
<textarea
class="ai-summary-textarea"
@ -492,7 +500,7 @@
<div class="ai-summary-edit">
<div class="ai-summary-header">
<span class="ai-summary-label">💡 AI 요약</span>
<button class="btn-primary btn-sm" onclick="regenerateSummary(2)">AI 재생성</button>
<button class="btn btn-primary btn-sm" onclick="regenerateSummary(2)">AI 재생성</button>
</div>
<textarea
class="ai-summary-textarea"
@ -569,7 +577,7 @@
<div class="ai-summary-edit">
<div class="ai-summary-header">
<span class="ai-summary-label">💡 AI 요약</span>
<button class="btn-primary btn-sm" onclick="regenerateSummary(3)">AI 재생성</button>
<button class="btn btn-primary btn-sm" onclick="regenerateSummary(3)">AI 재생성</button>
</div>
<textarea
class="ai-summary-textarea"

View File

@ -1429,11 +1429,12 @@ input[type="date"]::-webkit-calendar-picker-indicator {
.related-meeting-item {
background: var(--white);
border-radius: var(--radius-md);
padding: var(--space-sm);
padding: var(--space-md);
margin-bottom: var(--space-sm);
display: flex;
gap: var(--space-sm);
transition: background var(--transition-fast);
cursor: pointer;
transition: all var(--transition-fast);
border: 1px solid var(--gray-200);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.reference-item:last-child,
@ -1443,7 +1444,15 @@ input[type="date"]::-webkit-calendar-picker-indicator {
.reference-item:hover,
.related-meeting-item:hover {
background: var(--gray-100);
border-color: var(--primary);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
.reference-item:active,
.related-meeting-item:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.reference-content,

View File

@ -2131,8 +2131,8 @@ graph TD
| 1.4.18 | 2025-10-24 | 강지수 | 05-회의진행 실시간 주요 메모 추천 기능 명확화 (유저스토리 v2.1.1)<br>- **AI 제안 탭 기능 상세화**: 실시간 주요 메모 추천 기능 명시 추가<br> - UFR-MEET-030: 실시간 AI 주요 메모 추천<br> - 음성→텍스트 변환 후 AI가 실시간 분석<br> - **중요한 내용으로 판단된 경우에만** 주요 메모 항목 추천<br> - 추천 빈도는 중요 내용 발생에 따라 가변적 (3-5초 고정 간격 아님)<br> - 각 추천 항목에 "주요 메모에 추가" 버튼 제공<br> - 실시간 업데이트: 새로운 추천은 상단에 표시<br>- **프로토타입 확인**: 05-회의진행.html의 AI 제안 탭이 실시간 주요 메모 추천 기능을 포함하고 있음을 확인<br>- **참조**: design/uiux/요구사항설계검토-report-V1.2.md (실시간 주요 메모 추천 명시 부족 개선) |
| 1.4.19 | 2025-10-24 | 강지수 | 05-회의진행 화면 설계서 프로토타입 기준 전면 수정<br>- **레이아웃 구조 변경**: "2열 구조" 표현 제거, "메인 콘텐츠 영역: 정보 패널 (탭 구조)"로 단순화<br> - 텍스트 편집 영역 관련 내용 모두 제거 (왼쪽 영역, 에디터 툴바, contentEditable 등)<br> - 현재 프로토타입은 헤더 + 탭 콘텐츠 구조만 보유<br>- **반응형 디자인 명확화**: Mobile/Desktop 모두 동일한 구조에 너비만 반응형<br> - "2열 구조를 1열로 전환", "바텀시트" 표현 제거<br> - Mobile: 전체 너비 사용, Desktop: 최대 너비 제한 없이 반응형<br>- **AI 제안 탭 기능 명확화**: 논의항목/결정사항 구분 제거<br> - "논의항목/결정사항 등의 구분 없이 중요 내용을 주요 메모로 제안" 명시<br> - AI는 단순히 중요한 내용을 주요 메모 항목으로 제안하는 역할만 수행<br>- **용어 사전 검색 기능 추가**: 검색 입력창 + 검색 버튼<br> - Enter 키 지원, 용어명과 정의 모두 검색<br> - 검색 동작 상세 설명: 일치하는 용어만 표시, 하이라이트 효과, 결과 없으면 전체 목록 표시<br>- **인터랙션 섹션 정리**: 텍스트 편집, 툴바 사용, 충돌 감지 등 편집 관련 내용 모두 제거<br> - 탭 전환, 회의 종료, 실시간 업데이트만 유지<br> - 실시간 업데이트 항목을 현재 화면에 맞게 수정 (AI 제안, 용어 사전, 관련 회의록)<br>- **데이터 요구사항 업데이트**: 사용자 편집 내용 제거, 참석자 초대 이메일 추가<br> - AI 제안을 "주요 메모 항목 제안"으로 명확히 표현<br>- **에러 처리 업데이트**: 편집 충돌 에러 제거, 용어 사전 로드 실패/참석자 초대 실패 추가<br>- **주요 기능 목록 정리**: 실시간 협업/수동 편집 제거, AI 주요 메모 제안/참석자 관리 추가<br>- **권한 항목 수정**: "회의록 편집: 모든 참석자" → "참석자 초대: 모든 참석자"<br>- **프로토타입 기준 반영**: 05-회의진행.html 실제 구현 상태 100% 반영 |
| 1.4.20 | 2025-10-25 | 이미준, 강지수 | 유저스토리 v2.3.0 반영<br>- 회의 종료 화면 정책 명확화 (확인 전용, 바로 최종 확정 옵션 상세화)<br>- UFR-MEET-050: 최종 확정 2가지 시나리오 설명 추가<br>- UFR-COLLAB-020: 안건 기반 충돌 해결 메커니즘 상세 추가<br>- 실시간 협업 충돌 방지 정책 강화 |
| 1.5.2 | 2025-10-27 | 강지수 | AI 요약 기능 통합 및 단순화 (유저스토리 v2.4.1 반영)<br>- **11-회의록수정**: AI 요약 기능 통합<br> - 명칭 변경: "AI 상세 요약" → "AI 요약"<br> - AI 요약 영역: AI 한줄 요약만 표시 (30자 이내, 읽기 전용)<br> - 텍스트 편집 영역: 안건 내용 자유 작성 (논의 주제, 발언자별 의견, 결정 사항 등)<br> - "AI 재생성" 버튼: 텍스트 편집 영역 내용 기반으로 AI 요약의 한줄 요약 재생성 (2-5초 처리)<br> - 재생성된 한줄 요약은 회의록 상세조회 화면의 대시보드 및 회의록 탭에 즉시 반영<br> - AI 상세 요약 및 한줄 요약 분리 표시 제거<br>- **프로토타입 UI 개선**:<br> - AI 재생성 버튼 스타일 통일: btn-secondary → btn-primary (다른 화면과 일관성)<br> - 안건별 검증완료 UI 단순화: 참석자는 체크박스만, 회의 생성자는 검증완료 시 잠금해제 버튼 표시<br> - .creator-only CSS 클래스 추가: data-is-creator 속성 기반 표시 제어<br>- **관련 유저스토리**: UFR-AI-036 (AI 한줄요약 확인 및 재생성), UFR-MEET-055 (안건별 검증), UFR-COLLAB-030 (충돌 방지) |
| 1.5.1 | 2025-10-27 | 강지수 | MVP 스코프 축소 v2.4.0 반영 (4개 화면 수정)<br>- **02-대시보드**: Todo 위젯 및 통계 제거 (UFR-USER-020 반영)<br> - Todo 위젯 전체 제거 (나의 Todo 섹션 삭제)<br> - 통계 카드: "나의 Todo" 제거, "작성중 회의록" 유지 (2개 항목)<br> - 네비게이션: 하단 네비게이션 및 사이드바에서 Todo 관리 메뉴 제거<br> - Desktop 통계 그리드: 2개 항목만 표시<br>- **05-회의진행**: "AI 제안" 탭 → "AI 기반 메모" 탭 기능 변경<br> - 메모 입력창 + 저장 버튼 추가<br> - AI가 감지한 주요 내용 리스트 표시 (시간 + 내용)<br> - 각 참석자별 개별 저장, 다른 참석자 메모 볼 수 없음<br> - 메모는 회의 종료 전까지만 표시/편집 가능<br> - 에러 처리: AI 주요 내용 감지 실패, 메모 저장 실패 추가<br>- **10-회의록상세조회**: Todo 단순 조회 기능으로 변경<br> - Todo는 제목 + 담당자 + 마감일만 표시<br> - D-day 라벨, 우선순위 배지, 진행률 바, 상태별 필터 제거<br> - Todo 관리 화면 연동 링크 제거 (화면 자체가 제거됨)<br> - "수정" 버튼을 헤더로 이동<br>- **11-회의록수정**: 실시간 협업 기능 제거, 안건 기반 충돌 방지 강화<br> - "편집 중" 표시 제거 (실시간 협업 기능 제거)<br> - Todo 편집/추가/삭제 기능 전체 제거 (단순 조회만 가능)<br> - AI 한줄 요약 재생성 불가 (회의 종료 시 1회만 생성)<br> - 검증률 표시 및 최종 확정 버튼 제거<br> - 저장 로직 추가: 검증완료 안건 저장 스킵, 저장 결과 알림<br> - 안건별 검증 완료 체크박스로 충돌 방지 (Last Write Wins 적용)<br> - 에러 처리: 충돌 경고 모달 제거 (LWW로 인해) |
| 1.5.2 | 2025-10-27 | 강지수 | AI 요약 기능 통합 및 단순화 (유저스토리 v2.4.1 반영)<br>- **11-회의록수정**: AI 요약 기능 통합<br> - 명칭 변경: "AI 상세 요약" → "AI 요약"<br> - AI 요약 영역: AI 한줄 요약만 표시 (30자 이내, 읽기 전용)<br> - 텍스트 편집 영역: 안건 내용 자유 작성 (논의 주제, 발언자별 의견, 결정 사항 등)<br> - "AI 재생성" 버튼: 텍스트 편집 영역 내용 기반으로 AI 요약의 한줄 요약 재생성 (2-5초 처리)<br> - 재생성된 한줄 요약은 회의록 상세조회 화면의 대시보드 및 회의록 탭에 즉시 반영<br> - AI 상세 요약 및 한줄 요약 분리 표시 제거<br>- **프로토타입 UI 개선**:<br> - AI 재생성 버튼 스타일 통일: btn-secondary → btn-primary (다른 화면과 일관성)<br> - 안건별 검증완료 UI 단순화: 참석자는 체크박스만, 회의 생성자는 검증완료 시 잠금해제 버튼 표시<br> - .creator-only CSS 클래스 추가: data-is-creator 속성 기반 표시 제어<br>- **관련 유저스토리**: UFR-AI-036 (AI 한줄요약 확인 및 재생성), UFR-MEET-055 (안건별 검증), UFR-COLLAB-030 (충돌 방지) |
---

View File

@ -8,7 +8,7 @@ spring:
datasource:
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
username: ${DB_USERNAME:hgzerouser}
password: ${DB_PASSWORD:}
password: ${DB_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
@ -35,7 +35,7 @@ spring:
redis:
host: ${REDIS_HOST:20.249.177.114}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
password: ${REDIS_PASSWORD:Hi5Jessica!}
timeout: 2000ms
lettuce:
pool:
@ -51,7 +51,7 @@ server:
# JWT Configuration
jwt:
secret: ${JWT_SECRET:}
secret: ${JWT_SECRET:hgzero-jwt-secret-key-for-dev-environment-only-do-not-use-in-production-minimum-256-bits}
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
@ -125,5 +125,11 @@ api:
# Azure EventHub Configuration
eventhub:
connection-string: ${EVENTHUB_CONNECTION_STRING:}
name: ${EVENTHUB_NAME:hgzero-eventhub-name}
name: ${EVENTHUB_NAME:hgzero-events}
consumer-group: ${EVENTHUB_CONSUMER_GROUP:$Default}
# Azure Storage Configuration (for EventHub checkpoints)
azure:
storage:
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
container: ${AZURE_STORAGE_CONTAINER:hgzero-checkpoints}

Some files were not shown because too many files have changed in this diff Show More