서버에 접속했으니 이제 Docker 를 서버에 설치하고 

 

# 패키지 업데이트
sudo apt update

# Docker 설치
sudo apt install -y docker.io docker-compose

# Docker 권한 부여 (sudo 없이 사용)
sudo usermod -aG docker ubuntu

# 적용 (재접속 필요)
exit

순서대로 간다.

 

다시 접속

 

ssh -i ~/.ssh/healthsync-key.pem ubuntu@xxx.xxx.xxx.xxx

 

그리고 도커는 잘설치되었나 보자

 

오케이..

잘설치되었고 ...이제 프로젝트를 가져와야한다.. "Git"에서

 

git clone https://github.com/magui-dev/HealthSync-Public.git

프로젝트 설치된 내 깃허브 주소에서 클론한다.

 

 

 

그리고 폴더이동

cd HealthSync-Public

 

그리고여기 환경설정 이랑 여러 Oauth,openAi api, 등 키를 가져올 env 파일을 만들어야한다..

루트에 만들어놓은env 복붙할껀데... 먼저 리눅스에서 파일부터 만들자

 

nano .env

 

 

 

기존 작성해놓은 걸 복사 붙여넣기 한뒤에 

 

Ctrl + O  > Enter > Ctrl + X  순으로 저장하면된다 (맥 OS 라도 cmd 아니고 ctrl이다.)

 

어..근데..생각해보니 DataBase를 안넣었다.... (AI만 믿고 따라왔다가.. 깜놀했다. 근데 알아서 알려주네.)

 

우선 내 Mysql 버전을 먼저보고

 

 

 

내 PC에 docker-compose.yml에 추가했다.

안전하게 8.0 버전이면 된다고함

  # MySQL
  db:
    image: mysql:8.0
    container_name: healthsync-db
    environment:
      MYSQL_ROOT_PASSWORD: xxxx
      MYSQL_DATABASE: healthsync
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    restart: always
 
 
 #======
 # 요건 depends_on 만 추가 
 #====== 
  # 백엔드(Spring Boot)
  backend:
    build: ./backend
    container_name: healthsync-backend
    ports:
      - "8080:8080"
    env_file:
      - .env
    environment:
        - SPRING_PROFILES_ACTIVE=prod
    depends_on:
      - db            # ← 이거 추가!
    restart: always
```
 #======
 # 맨밑에
 #======
 volumes:
  mysql_data:

 

그리고 

git status

git add . 

git commit -m "DB환경 추가"

git push origin main

 

이순서로 깃에 올렸다.

 

그리고 서버에

git pull

처리했다.

 

그리고 

서버의 .env 파일을 수정 ..

# 변경 전
DB_URL=jdbc:mysql://host.docker.internal:3306/healthsync
# 변경 후
DB_URL=jdbc:mysql://db:3306/healthsync

 

이게 로컬환경에선 

[Mac]
├── MySQL (Homebrew) ← 여기 접근!
└── Docker
    ├── backend → host.docker.internal로 Mac의 MySQL 접근
    └── frontend

 

서버에선

[EC2]
└── Docker
    ├── db (MySQL 컨테이너) ← 여기 접근!
    ├── backend → db로 MySQL 컨테이너 접근
    └── frontend

 

위치가 달라서 수정이 필요하다고함.. ( nano .env 들어가서 수정했음)

 

쨋든 다시 빌드해야하니깐 

docker-compose up --build -d

 

한 5분 걸렸다.... 멈춘줄알았다... 

T3.micro <<< 젤저렴한 AWS 는 원래 느리단다...

 

그리고 도커 컴포즈에서 백엔드 부분에 아래내용을 추가했다. 스프링 application.yml 에서db를 못끌고와서 다시 수정했다. (빠르게넘어간다.)

    environment:
        - SPRING_PROFILES_ACTIVE=prod
        - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/healthsync?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
        - SPRING_DATASOURCE_USERNAME=root
        - SPRING_DATASOURCE_PASSWORD=1234

 

 

헛헛헛

성공!

 

 

...된줄 알았지만 ...서버가 멈췄다....

무료서버로는 뭘 할수가없구나

 

 

 

경로는 저기...에서 바꾸면된다.

지금 healthsync는 중지 "중" 이라서 변경이 안되고, 이미 중지된 기존 프로젝트로 예시를 들었음. 한 5~10분걸린다.

t3.small 정도로 바꿔야겠다. 

 

서버 상태 확인해보니 STATUS 모두 Up 이다..

docker ps

 

 

....

어마어마한 환경변수 오류로..정리 못했다... 어쨋든..

 

Google Cloud Console

http://healthsync-dev.duckdns.org:8080/login/oauth2/code/google

Kakao Developers

http://healthsync-dev.duckdns.org:8080/login/oauth2/code/kakao

Naver Developers

http://healthsync-dev.duckdns.org:8080/login/oauth2/code/naver

 

 

모두 리다이렉트 URI추가해서 로그인 성공..

 

이제 서버를 껏다 킬때마다 바뀌는 ip를 고정해야한다...

 

https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2#Addresses:

 

https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2#Addresses:

 

ap-northeast-2.console.aws.amazon.com

 

aws 에서 무료로 제공한다고함

 

여기서 할당 우측 주황버튼 그냥누르면 설정화면이 나온다.

 

서버에 올릴 인스턴스를 선택하면된다.

 

 

다음 dns 를 배정해준.. 곳으로 가서 연결되는 ip 주소만 수정하자.

 

https://www.duckdns.org/domains

 

Duck DNS

Duck DNS free dynamic DNS hosted on AWS news: login with Reddit is no more - legal request support us: become a Patreon

www.duckdns.org

 

 

수정했는데 바로 접속이 안되면... 내 브라우저 캐시문제일 테니 ..

 

 

여기서 클리어 호스트 캐시 누르고 해보자

 

 

 

마지막으로... https 설정만 추가하자

#도커 다운
docker-compose down

#Certbot 설치
sudo apt update
sudo apt install certbot -y

#아래주소가 내주소다 말해주기
sudo certbot certonly --standalone -d healthsync-dev.duckdns.org
#여기까지입력하면 본인 이메일 쓰고, Y 누르면된다.

 

부분의미

certbot Let's Encrypt 인증서 발급 도구
certonly 인증서만 발급
--standalone 자체 웹서버로 도메인 확인
-d healthsync-dev.duckdns.org 너의 도메인!

 

 

할거 많다....

 

 

nano frontend/nginx.conf

 

server {
    listen 80;
    server_name healthsync-dev.duckdns.org;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name healthsync-dev.duckdns.org;

    ssl_certificate /etc/letsencrypt/live/healthsync-dev.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/healthsync-dev.duckdns.org/privkey.pem;

    root /usr/share/nginx/html;
    index index.html;
    
    
    # 백엔드 API 프록시
    location /api/ {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # OAuth2 프록시
    location /oauth2/ {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /login/ {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}

요걸로 저 엔진x 를 수정한다.

 

nano docker-compose.yml

 

frontend:
    build: ./frontend
    container_name: healthsync-frontend
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - backend
    restart: always

요것도 프론트 부분 수정한다..

 

마지막으로... 재빌드 실행!

docker-compose build --no-cache frontend
docker-compose up -d

 

 

쉽지않다....

여러번 해봐야겠따 ...

 

 

 

 

띄우긴 했따....후..

 

도커컴포즈 빌드해보자 

docker-compose up --build

 

 

 

빌드 잘 끝났다. 여기서 시간이 다소 걸린다. 5~10분?

 

요래 멈춰있고 오류가 없으니깐 백그라운드로 실행하자..

 

 

#백그라운드로 실행 (콘솔에서 입력창을 사용할 수 있게함)
docker-compose up -d

 

도커에 잘올라가있다. 

 

 

실제 브라우저 테스트해보면

백엔드는 아래오류가 정상이고, 프론트는 localhost << 잘뜬다..

 

 

 

로컬에서 도커이미지+컨테이너 잘돌아가는거 확인했으니, 파란걸로 끄거나 

docker-compose down

도커를 닫아준다.

 

이제 aws에 바로 올려준다. 

ec2 인스턴스를 생성하자.

 

 

오른쪽에 인스턴스 시작(주황버튼)

 

 

 

서버이름쓰고, 우분투 선택

서버 24.04 자동으로 냅둔다.

 

 

인스턴스는 t3.micro 가 1Gib메모리인데... 스프링부트 + 리액트의 간단한 웹이니.. 이정도면 충분할 것 같다.

중요한 키 페어(로그인) 부분에서  오른쪽 파란색 새 키 페어 생성 (중요!) 이게 인증서처럼 키를 내 PC 에 저장해서 이걸로 접속해야한다.

 

 

키는 뭐 그냥이름쓰고

호환성이 좋은 RSA 선택하고.   프라이빗키는 ssh사용하니깐 .pem 을 하면된다. 

 

 

요래하면 어떤 파일이 저장되는데. 

 

나의경우 프로젝트 폴더 바깥에 두었다.

깃에 올라갈수도있으니 깃에 관리되는곳 밖에 두거나 따로모으던가 .....

(참고로 저기뒀다가 ai 한테 혼났다..... 한곳에 ssh키는 모아두랜다....나의경우 깃을 써서그런지 내 홈화면에 ".ssh" 폴더가 이미 있었따.)

 

 

우측 상단 편집! 누르기전에..아래 

HTTPS 체크해야 보안용이된다, 

포트범위에 8080 넣고, 원본에 0.0.0.0/0 넣는다.

 

 

포트용도

22 SSH 접속 ✅
80 HTTP (프론트) ✅
443 HTTPS ✅
8080 백엔드 API ← 추가!

 

 

우측 창에 인스턴스 시작 버튼 눌러서..

 

 

가운데 인스턴스 연결누르고...

 

 

아이피 주소가 제공되었다.

이제 터미널(윈도우는 cmd??)눌러서 

ssh -i ~/.ssh/healthsync-key.pem ubuntu@xxx.xxx.xxx.xxx

 

아이피를 입력하면 

 

 

너도나도읽고쓸수있다고 거절당했다...

| 숫자 | 의미 |
|------|------|
| 4 | 읽기 (read) |
| 2 | 쓰기 (write) |
| 1 | 실행 (execute) |
| 0 | 없음 |
```
400 = 4(나) + 0(그룹) + 0(다른사람)
644 = 6(나) + 4(그룹) + 4(다른사람)

 

chmod 400 ~/.ssh/healthsync-key.pem

나만 읽을수있다! 라고 권한설정을 먼저해주고 다시 위의 ssh 입력

 

 

들어왔따..던전에 들어온느낌이다..

 

 

 

 

 

 

기존 프로젝트에 http://localhost:8080 등으로 하드코딩된 부분들은 모수 상대경로로 수정했다..(이부분은 너무 많은 부분이라 세세하게 블로그 안했음) 

 

이제 도커에 올릴텐데.. 

그전에 환경변수 설정할때 여러번 하지않기위해 미리 도메인 무료로 받아올준비부터 한다.

 

https://desec.io/

 

deSEC – Free Secure DNS

 

desec.io

 

 

여기 페이지에서 일단 가입부터하자

 

 

내 구글계정으로 이메일넣고 , 위쪽 CREATE ACCOUNT 누르면 

 

 

무료로 내 dns로 만들 도메인을 만들어준다. 1개만 만들어준다고 하니 ...참고 ....(물론 하위로 몇개더만들순 있는모양이지만)

 

난 healthsync.dedyn.io 가 도메인 주소가 될예정이다. 

추후 저 위에 content 부분에  AWS에서 나올 EC2 IP 주소를 넣고 , 우측 TTL에 3600초, 1시간동안 dns결과를 캐싱한다는 뜻으로 그냥두면된다. 

 

 

이제 백엔드 DOCKER  설정을 시작해야함 ..

 

인텔리제이 기준 맨위에 프로젝트 루트에 오른쪽버튼 누르구..

 

 

저기 보이는 도커파일 누르면 자동으로 만들어짐

 

 

# ====================================
# 1단계 : 빌드 스테이지
# ====================================
FROM eclipse-temurin:17-jdk AS builder

WORKDIR /app

# Gradle Wrapper 복사
COPY gradlew ./
COPY gradle ./gradle

# 실행 권한 부여
RUN chmod +x gradlew

# Gradle 설정 파일 복사
COPY build.gradle settings.gradle ./

# 의존성 미리 다운로드
RUN ./gradlew dependencies --no-daemon || true

# 소스코드 복사 후 빌드
COPY src ./src
RUN ./gradlew bootJar --no-daemon -x test

# ====================================
# 2단계 : 실행 스테이지
# ====================================
FROM eclipse-temurin:17-jre

WORKDIR /app

# 빌드된 JAR만 복사
COPY --from=builder /app/build/libs/*.jar app.jar

# 포트 노출
EXPOSE 8080

# 실행
ENTRYPOINT ["java", "-jar", "app.jar"]




### 블로그용 설명

| 줄 | 의미 |
|-----|------|
| `FROM eclipse-temurin:17-jdk AS builder | Gradle + JDK 17로 빌드 환경 구성 |
| `WORKDIR /app` | 컨테이너 안에서 작업할 폴더 |
| `COPY build.gradle ...` | Gradle 설정 먼저 복사 (캐싱 효율) |
| `RUN gradle dependencies` | 의존성 미리 다운 (다음 빌드 시 캐시) |
| `COPY src ./src` | 소스코드 복사 |
| `RUN gradle bootJar` | JAR 파일 생성 (테스트 스킵) |
| `FROM eclipse-temurin:17-jre` | 실행용 가벼운 이미지 (JRE만) |
| `COPY --from=builder` | 빌드 스테이지에서 JAR만 가져옴 |
| `EXPOSE 8080` | 8080 포트 사용 |
| `ENTRYPOINT` | 컨테이너 시작 시 JAR 실행 |

---

### 왜 2단계로 나누나? (Multi-stage Build)
```
빌드 이미지: ~800MB (Gradle, JDK, 소스 전체)
      ↓
실행 이미지: ~200MB (JRE + JAR만)

 

 

 

이제 프론트엔드도 도커를 추가해야한다.. 먼저 프론트 노드버젼을 확인하자

 

내 터미널 경로> node -v
v24.11.1

 

그리고 도커는 슬림 으로~

 

#================================
# 1단계 : 빌드 스테이지
#================================
FROM node:22-slim AS builder

WORKDIR /app

# 패키지 파일 복사 (캐싱용)
COPY package.json package-lock.json ./

# 의존성 설치
RUN npm ci

# 소스코드 복사 후 빌드
COPY . .
RUN npm run build

#================================
# 2단계 : 실행 스테이지 (Nginx)
#================================
FROM nginx:stable

#빌드된 파일 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# 포트 노출
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

 

 

 

이제 ... 프론트까지 dockerfile 만들었으니...

이걸 통으로 휘두르는 docker-compose.yml 파일을.. 프론트, 백엔드 위의 루트에 만든다.

 

project/
├── backend/
├── frontend/
├── docker-compose.yml  ← 여기!
└── .env                ← 이것도 만들 예정

 

services:
  # 백엔드(Spring Boot)
  backend:
    build: ./backend
    container_name: healthsync-backend
    ports:
      - "8080:8080"
    env_file:
      - .env
    environment:
        - SPRING_PROFILES_ACTIVE=prod
    restart: always

  # 프론트엔드(Nginx)
  frontend:
    build: ./frontend
    container_name: healthsync-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    restart: always

 

 

다음은 도커가 읽을 환경 .env 파일만들기

 

 

# 데이터베이스
DB_URL=jdbc:mysql://host.docker.internal:3306/healthsync
DB_USERNAME=root
DB_PASSWORD=1234

# JWT
JWT_SECRET=여기에_아주_긴_시크릿키_넣기

# OpenAI
OPENAI_API_KEY=sk-proj-여기에_실제키

# OAuth Google
GOOGLE_CLIENT_ID=여기에_실제값
GOOGLE_CLIENT_SECRET=여기에_실제값

# OAuth Kakao
KAKAO_CLIENT_ID=여기에_실제값
KAKAO_CLIENT_SECRET=여기에_실제값

# OAuth Naver
NAVER_CLIENT_ID=여기에_실제값
NAVER_CLIENT_SECRET=여기에_실제값

# 클라이언트 URL
CLIENT_URL=http://localhost:5173

 

서버에 올라가서 활용할 env.example 추가하기

 

# 데이터베이스
DB_URL=
DB_USERNAME=
DB_PASSWORD=

# JWT
JWT_SECRET=

# OpenAI
OPENAI_API_KEY=

# OAuth Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# OAuth Kakao
KAKAO_CLIENT_ID=
KAKAO_CLIENT_SECRET=

# OAuth Naver
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=

# 클라이언트 URL
CLIENT_URL=

 

 

 

그리고 백엔드에  .dockerignore 를 추가

backend/
├── Dockerfile ✅
├── .dockerignore  ← 여기!
└── src/

 

# 빌드 결과물 (Docker 에서 새로 빌드함)
build/
.gradle/

# IDE 설정
.idea/
*.iml

# Git
.git/
.gitignore

# 기타
*.md
*.log

 

 

frontend/
├── Dockerfile ✅
├── .dockerignore  ← 여기!
└── src/

프론트에도 .dockerignore  추가

 

# 의존성 (Docker에서 새로 설치)
node_modules/

# 빌드 결과물
dist/
.vite/

# IDE
.idea/
*.iml

# Git
.git/
.gitignore

# 환경변수 (compose에서 관리)
.env
.env.local

# 기타
*.md
*.log

 

 

여기까지 하면 ... 기본적인 Docker , aws 준비완료...실전은 다음에..

 

 

##이번 포스팅은 무아지경으로 바이브코딩한거라..그대로 붙여넣을생각이니... 읽으시는분들은 흐름이 이상하다고 느끼신다면...
그게맞습니다...

브레인스토밍 모듈 트렌드 검색 통합 및 API 리팩토링

개요

브레인스토밍 아이디어 생성의 품질 향상을 위해 실시간 트렌드 검색 기능을 추가하고, 이를 실제 API에 적용하기 위한 아키텍처 리팩토링을 진행했다.

1. 트렌드 검색 기능 추가

1.1 트렌드 검색 소스 통합

3개의 트렌드 검색 소스를 통합하여 다양한 관점의 트렌드 키워드를 수집한다.

소스 파일 특징
네이버 뉴스 search/naver_news.py 국내 뉴스 기반 트렌드
DuckDuckGo search/duckduckgo.py 글로벌 검색 트렌드
네이버 데이터랩 search/naver_datalab.py 검색어 트렌드 통계

1.2 트렌드 검색 흐름

def fetch_trend_keywords(self, purpose: str) -> List[str]:
    all_keywords = []

    # 1. 네이버 뉴스 검색
    naver_keywords = await self.trend_searcher.extract_trend_keywords(purpose, num_articles=5)
    all_keywords.extend(naver_keywords)

    # 2. DuckDuckGo 검색
    ddg_keywords = await self.duckduckgo_searcher.extract_trend_keywords(purpose, num_articles=5)
    all_keywords.extend(ddg_keywords)

    # 3. 네이버 데이터랩 검색
    datalab_keywords = await self.datalab_searcher.extract_trend_keywords(purpose)
    all_keywords.extend(datalab_keywords)

    # 4. 중복 제거
    return list(dict.fromkeys(all_keywords))

1.3 트렌드 키워드 필터링

사용자 입력 키워드와의 유사도를 기반으로 관련성 높은 트렌드만 선별한다.

def filter_trend_keywords(self, trend_keywords: List[str], top_k: int = 10) -> List[str]:
    """
    트렌드 키워드를 사용자 입력 기준으로 필터링

    1. 트렌드 키워드 임베딩
    2. 사용자 키워드와 코사인 유사도 계산
    3. 상위 top_k개 선택
    """

2. 80:20 비중 적용

2.1 비중 설계 원칙

구분 비중 역할
사용자 키워드 80% 아이디어의 핵심 방향
트렌드 키워드 20% 시의성 추가

2.2 프롬프트 적용

【🔴 핵심: 사용자 브레인스토밍 키워드 (비중 80%)】
{사용자 키워드}

※ 위 키워드는 사용자가 직접 떠올린 것입니다. 이 키워드를 중심으로 아이디어를 구성하세요.

【🔵 참고: 최신 트렌드 키워드 (비중 20%)】
{트렌드 키워드}

※ 트렌드는 참고만 하세요. 사용자 키워드가 핵심입니다.

3. 새로운 출력 형식

3.1 기존 형식

핵심문제 → 개선방안 → 기대효과 → 발상기법 → 분석결과

3.2 변경된 형식

아이디어 제목: [제목]
주제: [어떤 문제/니즈를 해결하는지]
실행 방향: [무엇을 할지 - 구체적 도구나 수치 단정 금지]
확인 필요 사항: [실행 전 조사해봐야 할 것들]
기대효과: [예상 결과 - 숫자 단정 금지]
적용된 기법: [기법명]

3.3 할루시네이션 방지 규칙

❌ 특정 도구/서비스의 기능을 단정짓기 금지
   예: "네이버 클로바 API가 자동으로 자막 생성"
❌ 통계, 비용, 시장규모 지어내기 금지
   예: "월 100만원 수익 가능", "시장 규모 40조"
✅ 모르는 건 "확인 필요"로 표시
   예: "플랫폼별 최적 길이 조사 필요"

4. 평가 결과

4.1 GPT-5 Judge 기준

항목 Before After 변화
RAG 활용도 기준점 +1.2 향상
전체 점수 기준점 +0.18 향상

4.2 실용적 창의성 관점

항목 Before After 변화
조합 창의성 5.3 6.3 +1.0
트렌드 시의성 4.3 6.3 +2.0
구조적 완성도 6.0 9.0 +3.0
신뢰성 5.0 8.0 +3.0
전체 5.6 7.4 +1.8

5. API 아키텍처 리팩토링

5.1 문제 발견

트렌드 검색 기능을 idea_generator.py에 구현했으나, 실제 API 엔드포인트는 이를 호출하지 않고 자체 로직을 사용하고 있었다.

실제 API 경로:
brainstorming-popup.html
    ↓
brainstormingService.js
    ↓
endpoints/brainstorming.py  ← 직접 OpenAI 호출 (약 400줄)
                              ❌ idea_generator.py 호출 안 함
                              ❌ 트렌드 검색 없음

5.2 파일별 상태

파일 트렌드 검색 새 프롬프트 역할
idea_generator.py 콘솔 테스트용
runner.py 평가용
brainstorming.py 실제 API

5.3 리팩토링 구조

Before:

endpoints/brainstorming.py
    ↓
    직접 OpenAI 호출 (~400줄)

After:

endpoints/brainstorming.py  → 라우팅만 (~30줄)
    ↓
domain/brainstorming/idea_generator.py  → 실제 로직
    ├─ 트렌드 검색 (3개 소스)
    ├─ 키워드 추출
    ├─ 영구 RAG 검색
    ├─ 아이디어 생성 (새 프롬프트)
    └─ SWOT 분석

5.4 idea_generator.py에 추가된 API용 메서드

async def generate_ideas_for_api(
    self, 
    session_id: str, 
    purpose: str, 
    associations: List[str]
) -> List[Dict]:
    """API에서 호출 가능한 아이디어 생성 메서드"""

    # 1. Ephemeral RAG 초기화
    ephemeral_rag = EphemeralRAG(session_id=session_id)

    # 2. 연상 키워드 추출 (유사도 기반)
    keywords_data = ephemeral_rag.extract_keywords_by_similarity(purpose=purpose, top_k=7)

    # 3. 트렌드 키워드 검색 (3개 소스)
    trend_keywords = await self._fetch_trend_keywords_async(purpose)

    # 4. 트렌드 키워드 필터링
    trend_keywords = ephemeral_rag.filter_trend_keywords(trend_keywords, top_k=10)

    # 5. 영구 RAG에서 브레인스토밍 기법 검색
    techniques_results = self._search_permanent_rag_for_api(query=purpose, n_results=3)

    # 6. 아이디어 생성
    ideas = self._generate_ideas_with_prompt(purpose, keywords, techniques, trend_keywords)

    # 7. SWOT 분석 추가
    for idea in ideas:
        swot = self._perform_swot_analysis(idea)
        idea['analysis'] = swot_text

    return ideas

5.5 brainstorming.py 엔드포인트 수정

@router.get("/ideas/{session_id}", response_model=IdeaResponse)
async def generate_ideas(session_id: str):
    """
    idea_generator.py의 generate_ideas_for_api 메서드를 호출
    (트렌드 검색 + 80:20 비중 + 할루시네이션 방지 적용)
    """
    session = session_manager.get_session(session_id)
    purpose = session.get('q1_purpose')
    associations = session.get('q3_associations', [])

    from idea_generator import IdeaGenerator
    generator = IdeaGenerator()
    ideas = await generator.generate_ideas_for_api(
        session_id=session_id,
        purpose=purpose,
        associations=associations
    )

    return IdeaResponse(ideas=ideas)

5.6 config.py 수정

네이버 검색 API 키 필드 추가:

# Naver Search API (트렌드 검색용)
NAVER_SEARCH_CLIENT_ID: str = ""
NAVER_SEARCH_CLIENT_SECRET: str = ""

6. 테스트 결과

6.1 서버 로그

[API] 아이디어 생성 시작 - 세션: c92d5acd-...
[API] 추출된 키워드: ['여러가지', '글을 쓸거에요', ...]
[API] 네이버 뉴스: 8개
[API] DuckDuckGo: 8개
[API] 네이버 데이터랩: 6개
[API] 트렌드 키워드 (필터링 전): 22개
[API] 트렌드 키워드 (필터링 후): ['K-유통 체험', '사용자 생성 콘텐츠', ...]
   💡 감지된 직군: 크리에이터
[API] 아이디어 생성 완료: 3개

6.2 검증 항목

항목 상태
트렌드 검색 3개 소스 작동
트렌드 필터링
직군 감지
SWOT 분석
API → idea_generator 호출

7. 파일 구조

backend/app/domain/brainstorming/
├── idea_generator.py          # 아이디어 생성 (콘솔 + API 공용)
├── ephemeral_rag.py           # JSON 기반 임시 RAG
├── domain_hints.py            # 직군별 힌트
├── search/
│   ├── naver_news.py          # 네이버 뉴스 검색
│   ├── duckduckgo.py          # DuckDuckGo 검색
│   └── naver_datalab.py       # 네이버 데이터랩 검색
└── evaluation/
    └── runner.py              # 평가용 러너

backend/app/api/v1/endpoints/
└── brainstorming.py           # API 엔드포인트 (라우팅만)

8. 교훈

  1. 콘솔 테스트 ≠ 실제 서비스: 별도 코드베이스면 개선이 전파되지 않음
  2. 엔드포인트는 라우팅만: 비즈니스 로직을 엔드포인트에 직접 구현하면 재사용 불가
  3. 책임 분리: 도메인 로직과 API 레이어를 명확히 분리해야 유지보수 가능
  4. 평가 기준의 한계: GPT-5 Judge는 "파괴적 혁신"만 높이 평가, 실용적 조합 창의성은 저평가

9. 향후 작업

  • runner.py 리팩토링 (idea_generator.py import 방식으로 전환)
  • 창의성 점수 향상 방안 연구 (트렌드 비중 조정, 기법 다양화)

 

 

국비학원 최종 프로젝트였던 HealthSync ...

 

비록 나름 고심해서 꽤나 로직에 공들였지만서도,공공기관 데이터 화재사건으로 ..문제도있었고..

당시 가장마음에 걸렸떤게 바로... 서버에 올리지 못했던것!

 

두번째로 마음에 걸리는 "챗봇" 의 1회성 답변을 수정하고 서버에 올리는 걸로 목표를 잡았다.

 

 

챗봇을 보면 대화가 이어지는것 처럼 보이지만,

>> 안녕 난 헤비스모커야     (컨텍스트제공)
 
 챗>> 예상답변으로 안녕 헤비스모커야~?    (컨텍스트를 활용해서 답변해야하지만 ... 인사도 건강주제가 아니라고 답변을 안해줌)

 

그리고 대화가 이어지지 않는다는 증거로는 .. 

 >> 내가 누구라고 ? ( 위쪽 컨텍스트에대해서 답변을 요구)
 
챗>>  예상답변으로 헤비스모커라고 말해주길 기대했지만, 위의 답변을 싹 무시하고 [로직에 정의된대로 왼쪽 프로필정보를 읊어준다.]

 

 

이 2가지 문제를 해결해야겠다. 

 

 

 

문제 1. 인사도 안받아줌 ...

 

private OpenAiRequest getOpenAiRequest(String context, String prompt) {
        String healthSyncPrompt = "너는 'HealthSync' 서비스 소속의 전문 헬스케어 및 식단 관리 AI 어시스턴트야. " +
                "[너의 핵심 임무] " +
                "사용자에게 건강한 식단, 운동 방법, 영양 정보, 스트레스 관리법에 대해 과학적 근거를 바탕으로 조언해야 해. " +
                "항상 사용자의 건강 목표 달성을 돕는 것을 최우선으로 생각해. " +
                "[답변 규칙] " +
                "1. 절대로 의료적 진단, 질병의 원인 규명, 의약품 처방 및 추천을 해서는 안 돼. 사용자가 진단을 요구하면, '저는 의료 전문가가 아니므로 정확한 진단은 병원을 방문하여 의사와 상담하시는 것을 강력히 권장합니다.'라고 답변해야 해. " +
                "2. 답변은 항상 친절하고 긍정적인 톤을 유지해 줘. " +
                "3. 건강, 운동, 식단과 전혀 관련 없는 주제(예: 정치, 금융, 연예, 기술 등)에 대한 질문은 정중하게 거절해. '저는 건강 및 식단 전문 AI라서 해당 주제에 대해서는 답변하기 어려워요. 건강 관련 질문이 있으시면 언제든지 말씀해주세요!'와 같이 답변해. " +
                "4. 모든 답변은 5문단의 길이로 요약해서 제공해 줘." +

                "\n\n[답변 형식 규칙]" +
                "\n1. 답변의 가독성을 높이기 위해 마크다운(Markdown)을 적극적으로 사용해줘." +
                "\n2. 각 식사(아침, 점심, 저녁, 간식)는 '### 아침', '### 점심'과 같이 마크다운 제목(헤딩 3단계)으로 명확히 구분해줘." +
                "\n3. 중요한 키워드나 음식 이름은 `**`로 감싸서 **굵은 글씨**로 강조해줘." +
                "\n4. 식단 예시처럼 나열이 필요한 정보는 반드시 `-` 기호를 사용한 목록(리스트) 형식으로 정리해줘.";

 

 

프롬프트 엔지니어링에서 너무 빡빡한 내용이 있음 

 

""3. 건강, 운동, 식단과 전혀 관련 없는 주제(예: 정치, 금융, 연예, 기술 등)에 대한 질문은 정중하게 거절해. '저는 건강 및 식단 전문 AI라서 해당 주제에 대해서는 답변하기 어려워요. 건강 관련 질문이 있으시면 언제든지 말씀해주세요!'와 같이 답변해. " +"

 

 

당시에는 api 비용이 나갈것을 우려해서 최대한 소극적으로 했지만..아무래도 챗봇인데 너무 정없다...(?)

 

    private OpenAiRequest getOpenAiRequest(String context, String prompt) {
        String healthSyncPrompt =
                "너는 'HealthSync' 서비스 소속의 전문 헬스케어 및 식단 관리 AI 어시스턴트야. " +

                        "[너의 핵심 임무] " +
                        "사용자에게 건강한 식단, 운동 방법, 영양 정보, 스트레스 관리법에 대해 과학적 근거를 바탕으로 조언해야 해. " +
                        "항상 사용자의 건강 목표 달성을 돕는 것을 최우선으로 생각해. " +

                        "[답변 규칙] " +
                        "1. 절대로 의료적 진단, 질병의 원인 규명, 의약품 처방 및 추천을 해서는 안 돼. " +
                        "2. 답변은 항상 친절하고 긍정적인 톤을 유지해 줘. " +

                        // ✅ 개선: 유연하면서도 절제하는 형태
                        "3. 인사나 일반적인 대화는 친절하게 받아줘. " +
                        "   예: '안녕하세요!' → '안녕하세요! 건강 관련 도움이 필요하신가요?'" +
                        "4. 사용자와 대화를 나누되, 만약 대화가 건강과 지속적으로 무관한 방향으로 진행되면 " +
                        "   부드럽게 건강 주제로 유도해. 예: '좋은 말씀이네요. 그런데 혹시 식단이나 운동으로 도움을 드릴 만한 게 있을까요?'" +
                        "5. 명백하게 건강과 무관한 주제(정치, 금융, 기술 심화 등)의 지속적인 질문에는 " +
                        "   정중하게 '저는 건강 및 식단 전문 AI라서...'라고 표현해. " +
                        "6. 모든 답변은 5문단의 길이로 요약해서 제공해 줘."+

 

 

사용자와 대화를 나누되, ... 너무 건강관련해서 대화주제가 다른곳으로가면 정중히 건강주제로 유도하면서 거절하도록 했다. 

 

 

담배피는것은 좋지않다고한다...

이정도면 인사는 잘받아주는거같으니 굳..

 

 

문제 2. 기억상실증

 

 

사용자의 요청 -> 답변은 잘하는데...

 

바로 전 질문이 뭔지를 신경쓰지않는다.

 

이 짧은 메서드에서 1번으로 대화를 받아오고.. setp1.으로 대화를 뱉어낼뿐...

    public String getAnswer(Long userId, ChatRequest chatRequest) {

        // 1. 프론트에서 받은 컨텍스트 데이터 DTO를 가져옵니다.
        ReportContextDto contextDto = chatRequest.getReportContext();

        // 2. 받은 DTO가 null인지 확인 (선택적 예외 처리)
        if (contextDto == null) {
            // 컨텍스트 없이 일반적인 답변을 하거나, 오류를 반환할 수 있습니다.
            // 여기서는 컨텍스트 없이 질문만 넘깁니다.
            OpenAiResponse openAiResponse = openAiClient.getChatCompletion(null, chatRequest.getMessage());
            return openAiResponse.getChoices().get(0).getMessage().getContent();
        }

        // 3. DTO를 AI가 이해하기 쉬운 문자열로 변환합니다.
        String context = buildUserContext(contextDto);

        // step 1. OpenAiClient를 통해 질문을 OpenAI에 전달
        log.info("Request from User ID: {}, Question: {}", userId, chatRequest.getMessage());
        OpenAiResponse openAiResponse = openAiClient.getChatCompletion(context, chatRequest.getMessage());
        return openAiResponse.getChoices().get(0).getMessage().getContent();

    }

 

 

이걸 해결하기 위해서 몇가지 추가적인 걸 작업해야한다.

차근차근 해보면될듯....

1️⃣ Entity/DB 설계 필요
   └─ ChatHistory (메시지 저장)
   └─ ChatSummary (요약 저장)

2️⃣ Repository 필요
   └─ ChatHistoryRepository
   └─ ChatSummaryRepository

3️⃣ ChatService.getAnswer() 
   └─ Dequeue 로직
   └─ 15개 체크
   └─ 요약 생성 로직
   └─ 저장 로직

4️⃣ 요약 생성 (새로운 메서드)
   └─ OpenAI에 "처음 5개 메시지 요약해줘" 호출
   └─ 요약 받음

5️⃣ OpenAiClient.getChatCompletion()
   └─ 히스토리 + 요약 함께 전달

6️⃣ 새 대화 시작 시 요약 삭제
   └─ 언제 "새 대화"인지 판단 로직 필요

 

 
 
 

 

원했던 내용인 이정도로 ... 

 

📋 구현 완료 요약

생성/수정된 파일들

파일상태설명

ChatHistory.java ✅ 수정 실시간 대화 저장 엔티티
ChatSummary.java ✅ 생성 요약 저장 (UUID_날짜 ID)
ChatHistoryRepository.java ✅ 생성 대화 CRUD
ChatSummaryRepository.java ✅ 생성 요약 CRUD + 날짜별 정리
ChatService.java ✅ 수정 히스토리 관리 + 요약 로직
OpenAiClient.java ✅ 수정 messages에 히스토리 포함

 


동작 흐름

 
 
1️⃣ 사용자 질문 들어옴
   ↓
2️⃣ cleanupOldSummaries() - 오늘이 아닌 요약 자동 삭제
   ↓
3️⃣ buildConversationHistory() - 오늘 요약 + 현재 히스토리 조합
   ↓
4️⃣ saveUserMessage() - 사용자 질문 DB 저장
   ↓
5️⃣ OpenAI API 호출 (system + 요약들 + 히스토리 + 현재 질문)
   ↓
6️⃣ saveAssistantMessage() - AI 응답 DB 저장
   ↓
7️⃣ checkAndSummarize() - 15개 이상이면 5개 요약 후 삭제

 

..쩝

이제 담번에 서버올려보는거롤 마무리하자 

🚀 AWS EC2 배포 완료! - Docker + Nginx + HTTPS 풀스택 배포기

🎯 오늘의 목표

드디어... 배포다! 로컬에서만 돌리던 서비스를 진짜 인터넷에 올리는 날!

로컬호스트 감옥 탈출 🏃‍♂️
↓
https://idea-brainstorm.duckdns.org ✨

✨ 완성된 것들

  • ✅ AWS EC2 t3.medium 인스턴스 설정
  • ✅ Docker Compose로 4개 컨테이너 배포
  • ✅ DuckDNS 무료 도메인 연결
  • ✅ Nginx 리버스 프록시 설정
  • ✅ Let's Encrypt HTTPS 적용
  • ✅ Google / Naver / Kakao OAuth 연동
  • ✅ 브레인스토밍 기능 (Python API) 연동

결과: https://idea-brainstorm.duckdns.org 에서 실제 서비스 운영 중! 🎉


📚 최종 시스템 구조

┌─────────────────────────────────────────────────────────────┐
│                    사용자 브라우저                            │
│           https://idea-brainstorm.duckdns.org               │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    AWS EC2 (t3.medium)                      │
│                    Ubuntu 24.04 LTS                         │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                 Docker Compose                         │ │
│  │  ┌─────────────────────────────────────────────────┐   │ │
│  │  │              Nginx (포트 80, 443)               │   │ │
│  │  │  • SSL/TLS 인증서 (Let's Encrypt)               │   │ │
│  │  │  • HTTP → HTTPS 리다이렉트                      │   │ │
│  │  │  • 프론트엔드 정적 파일 서빙                     │   │ │
│  │  │  • /api/ → Spring Boot 프록시                  │   │ │
│  │  │  • /api/v1/brainstorming/ → Python 프록시      │   │ │
│  │  └─────────────────────────────────────────────────┘   │ │
│  │           │                          │                  │ │
│  │           ▼                          ▼                  │ │
│  │  ┌─────────────────┐      ┌─────────────────────┐      │ │
│  │  │ Spring Boot     │      │ Python FastAPI      │      │ │
│  │  │ (포트 8080)     │      │ (포트 8000)         │      │ │
│  │  │ • OAuth2 + JWT  │      │ • OpenAI GPT-4      │      │ │
│  │  │ • User CRUD     │      │ • Ephemeral RAG     │      │ │
│  │  │ • Idea CRUD     │      │ • 브레인스토밍 엔진   │      │ │
│  │  │ • Inquiry CRUD  │      │ • SWOT 분석         │      │ │
│  │  └────────┬────────┘      └─────────────────────┘      │ │
│  │           │                                             │ │
│  │           ▼                                             │ │
│  │  ┌─────────────────┐                                   │ │
│  │  │ MySQL 8.0       │                                   │ │
│  │  │ (포트 3306)     │                                   │ │
│  │  │ • users         │                                   │ │
│  │  │ • ideas         │                                   │ │
│  │  │ • inquiries     │                                   │ │
│  │  └─────────────────┘                                   │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

🔥 1. AWS EC2 인스턴스 설정

첫 번째 삽질: t3.micro의 한계 💥

처음엔 프리티어 t3.micro (1GB RAM)로 시작했는데...

Docker 컨테이너 3개 실행
↓
메모리 부족
↓
서버 과부하 💀
↓
SSH 접속도 안 됨...

EC2 콘솔에서 강제 재부팅해야 했음 😱

해결: t3.medium 업그레이드

t3.micro  (1GB RAM)  → 💀 메모리 부족
t3.medium (4GB RAM)  → ✅ 쾌적!

비용: 월 약 $33 (프리티어 아님... 😢)

하지만 4개 컨테이너 돌리려면 어쩔 수 없음!

인스턴스 유형 변경 방법

1. EC2 콘솔 → 인스턴스 선택
2. 인스턴스 상태 → 중지
3. 작업 → 인스턴스 설정 → 인스턴스 유형 변경
4. t3.medium 선택
5. 다시 시작

⚠️ 주의: IP 주소가 바뀔 수 있음! (탄력적 IP 사용 권장)


🐳 2. Docker Compose 설정

docker-compose.yml

services:
  mysql:
    image: mysql:8.0
    container_name: brainstorm-mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=brainstorm
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - brainstorm-network

  python-service:
    build:
      context: .
      dockerfile: Dockerfile.python
    container_name: brainstorm-python
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - brainstorm-network

  spring-boot:
    build:
      context: .
      dockerfile: Dockerfile.spring
    container_name: brainstorm-spring
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/brainstorm?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
      - SPRING_DATASOURCE_USERNAME=${MYSQL_USER}
      - SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
      - KAKAO_CLIENT_ID=${KAKAO_CLIENT_ID}
      - KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET}
      - NAVER_CLIENT_ID=${NAVER_CLIENT_ID}
      - NAVER_CLIENT_SECRET=${NAVER_CLIENT_SECRET}
      - PYTHON_API_URL=http://python-service:8000
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - brainstorm-network

  nginx:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    container_name: brainstorm-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - spring-boot
      - python-service
    networks:
      - brainstorm-network

volumes:
  mysql_data:

networks:
  brainstorm-network:
    driver: bridge

컨테이너 상태 확인

docker compose ps
NAME                IMAGE                    STATUS                    PORTS
brainstorm-mysql    mysql:8.0                Up (healthy)              0.0.0.0:3306->3306/tcp
brainstorm-python   brainstorm-python        Up (healthy)              0.0.0.0:8000->8000/tcp
brainstorm-spring   brainstorm-spring        Up                        0.0.0.0:8080->8080/tcp
brainstorm-nginx    brainstorm-nginx         Up                        0.0.0.0:80->80/tcp, 443->443/tcp

4개 모두 Up! ✅


🌐 3. DuckDNS 무료 도메인

왜 DuckDNS?

유료 도메인: 연 1~2만원 💸
DuckDNS: 영구 무료! 🆓

개인 프로젝트엔 이거면 충분!

설정 방법

  1. https://www.duckdns.org 접속
  2. Google 계정으로 로그인
  3. 도메인 이름 입력: idea-brainstorm
  4. EC2 Public IP 입력: 15.164.177.90
  5. add domain 클릭

결과: idea-brainstorm.duckdns.org 도메인 획득! 🎉

토큰 저장 (자동 갱신용)

토큰: 938e56a9-xxxx-xxxx-xxxx-xxxxxxxxxxxx

나중에 IP 바뀌면 이 토큰으로 자동 업데이트 가능!


🔧 4. Nginx 리버스 프록시

왜 Nginx가 필요한가?

사용자 → Nginx → Spring Boot (API)
              → Python (브레인스토밍)
              → 정적 파일 (HTML/CSS/JS)

하나의 도메인으로 여러 서비스 접근 가능!

nginx.conf

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    upstream spring-boot {
        server brainstorm-spring:8080;
    }

    upstream python-api {
        server brainstorm-python:8000;
    }

    # HTTP → HTTPS 리다이렉트
    server {
        listen 80;
        server_name idea-brainstorm.duckdns.org;
        return 301 https://$host$request_uri;
    }

    # HTTPS 서버
    server {
        listen 443 ssl;
        server_name idea-brainstorm.duckdns.org;

        ssl_certificate /etc/letsencrypt/live/idea-brainstorm.duckdns.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/idea-brainstorm.duckdns.org/privkey.pem;

        # 프론트엔드 정적 파일
        location / {
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri $uri/ /index.html;
        }

        # Python 브레인스토밍 API (구체적인 경로 먼저!)
        location /api/v1/brainstorming/ {
            proxy_pass http://python-api;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Spring Boot API
        location /api/ {
            proxy_pass http://spring-boot;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # OAuth2
        location /oauth2/ {
            proxy_pass http://spring-boot;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # OAuth Callback
        location /login/ {
            proxy_pass http://spring-boot;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

⚠️ 중요: location 순서!

# ✅ 올바른 순서 (구체적인 것 먼저!)
location /api/v1/brainstorming/ { ... }  # 1번
location /api/ { ... }                    # 2번

# ❌ 잘못된 순서
location /api/ { ... }                    # 이게 먼저 매칭되면
location /api/v1/brainstorming/ { ... }  # 여기 안 옴!

Nginx는 먼저 매칭되는 location으로 라우팅함!


🔒 5. HTTPS 설정 (Let's Encrypt)

왜 HTTPS?

OAuth 로그인 → HTTPS 필수! (Google 정책)
보안 → 데이터 암호화
SEO → 검색 엔진 우대

Certbot으로 인증서 발급

# 1. Certbot 설치
sudo apt update
sudo apt install -y certbot

# 2. Nginx 잠시 중지 (80 포트 필요)
docker compose stop nginx

# 3. 인증서 발급
sudo certbot certonly --standalone \
  --preferred-challenges http \
  -d idea-brainstorm.duckdns.org

# 4. 이메일 입력, 약관 동의

결과:

Certificate: /etc/letsencrypt/live/idea-brainstorm.duckdns.org/fullchain.pem
Key: /etc/letsencrypt/live/idea-brainstorm.duckdns.org/privkey.pem
만료일: 2026-03-04 (3개월 후)

docker-compose.yml에 인증서 마운트

nginx:
  volumes:
    - /etc/letsencrypt:/etc/letsencrypt:ro  # 읽기 전용!

이제 HTTPS 작동! 🔒


🔑 6. OAuth 설정 삽질기

문제 1: redirect_uri_mismatch 😱

오류 400: redirect_uri_mismatch
요청한 URI: http://idea-brainstorm.duckdns.org/login/oauth2/code/google

원인: Google Console에 등록된 URI와 불일치!

해결:

Google Cloud Console → OAuth 클라이언트 ID
→ 승인된 리디렉션 URI 추가:
  https://idea-brainstorm.duckdns.org/login/oauth2/code/google

문제 2: http로 리다이렉트 됨 🤔

HTTPS 설정했는데 OAuth 리다이렉트가 http://로 감...

사용한 URI: http://idea-brainstorm.duckdns.org/...
                   ^^^^
                   왜 http지?

원인: Spring Boot가 원래 요청이 HTTPS인지 모름!

해결 1: application.yaml에 추가

server:
  port: 8080
  forward-headers-strategy: native  # ✅ 프록시 헤더 인식!

해결 2: Nginx에서 헤더 전달

location /oauth2/ {
    proxy_pass http://spring-boot;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;  # ✅ 이게 핵심!
}

X-Forwarded-Proto: https → Spring Boot가 HTTPS로 인식!

문제 3: Kakao 설정 오류 (KOE006)

앱 관리자 설정 오류 (KOE006)
등록하지 않은 리다이렉트 URI를 사용

해결:

Kakao Developers → 앱 → 플랫폼 키 → 카카오 로그인 리다이렉트 URI
→ https://idea-brainstorm.duckdns.org/login/oauth2/code/kakao 추가

최종 OAuth 설정 체크리스트

프로바이더 Console URL Redirect URI
Google console.cloud.google.com/apis/credentials https://...duckdns.org/login/oauth2/code/google
Naver developers.naver.com/apps https://...duckdns.org/login/oauth2/code/naver
Kakao developers.kakao.com/console/app https://...duckdns.org/login/oauth2/code/kakao

3개 다 설정 완료! ✅


🧠 7. 브레인스토밍 연동 문제

문제: "세션 시작에 실패했습니다"

브라우저에서 브레인스토밍 시작하면 에러...

원인 분석

프론트엔드 코드:

// ❌ 로컬에서만 작동
const API_BASE_URL = 'http://localhost:8000/api/v1/brainstorming';

서버에서 localhost:8000은 접근 불가! 💥

해결

// ✅ Nginx 프록시 경유
const API_BASE_URL = '/api/v1/brainstorming';

그리고 Nginx에서 프록시 설정:

location /api/v1/brainstorming/ {
    proxy_pass http://python-api;
    ...
}

흐름:

브라우저 → /api/v1/brainstorming/session
         → Nginx가 프록시
         → Python 컨테이너 (brainstorm-python:8000)

테스트

curl http://localhost/api/v1/brainstorming/session -X POST
{
  "session_id": "f2d9aeaf-d160-4aff-b402-3348cdb0a85b",
  "message": "새로운 브레인스토밍 세션이 시작되었습니다."
}

브레인스토밍 작동! ✅


👑 8. 관리자 계정 설정

문제: OAuth로 로그인하면 일반 사용자

DB 확인:

SELECT user_id, email, role, provider FROM users 
WHERE email='kimof4@gmail.com';
+---------+------------------+-------+----------+
| user_id | email            | role  | provider |
+---------+------------------+-------+----------+
|       1 | kimof4@gmail.com | ADMIN | LOCAL    |  ← 로컬 관리자
|       2 | kimof4@gmail.com | USER  | GOOGLE   |  ← Google 로그인
+---------+------------------+-------+----------+

같은 이메일인데 계정이 2개! 😅

해결: Google 계정을 ADMIN으로 변경

UPDATE users SET role='ADMIN' WHERE user_id=2;

로그아웃 → 재로그인 → 새 JWT 토큰에 ADMIN role 포함!

이제 /admin.html 접근 가능! 👑


🛡️ 9. 보안 설정

AWS 보안 그룹

포트 용도 상태
22 SSH ✅ 필요
80 HTTP → HTTPS ✅ 필요
443 HTTPS ✅ 필요
3306 MySQL ❌ 제거! (외부 차단)
8080 Spring Boot ❌ 제거 권장

MySQL 외부 접근 차단 필수! DB 직접 접근되면 보안 끝남 💀

환경 변수로 비밀 관리

# .env 파일 (서버에만 존재, Git에 올리면 안 됨!)
MYSQL_ROOT_PASSWORD=강력한비밀번호
MYSQL_USER=brainstorm
MYSQL_PASSWORD=강력한비밀번호
JWT_SECRET=256비트이상시크릿키
GOOGLE_CLIENT_SECRET=구글시크릿
OPENAI_API_KEY=sk-xxxxx

Git Push Protection 발동! 😱

remote: - GITHUB PUSH PROTECTION
remote:   Push cannot contain secrets
remote:   - Google OAuth Client ID
remote:   - Google OAuth Client Secret
remote:   - OpenAI API Key

nginx.conf.save 파일에 비밀키가 포함됨!

해결:

rm nginx/nginx.conf.save  # nano 백업 파일 삭제
git reset HEAD~1
git add docker-compose.yml Dockerfile.nginx nginx/nginx.conf
git commit -m "Add nginx HTTPS config"
git push origin main

GitHub가 비밀키 푸시를 막아줌 → 오히려 고마웠음 😅


😵 10. 삽질 총정리

삽질 1: t3.micro 메모리 부족

증상: 서버 먹통, SSH 접속 불가
원인: 1GB RAM으로 Docker 4개 무리
해결: t3.medium (4GB) 업그레이드
교훈: 프리티어의 한계... 💸

삽질 2: localhost 하드코딩

증상: 서버에서 API 호출 실패
원인: http://localhost:8000 하드코딩
해결: /api/v1/brainstorming 상대 경로로 변경
교훈: 배포 환경 고려해서 코드 작성!

삽질 3: OAuth redirect_uri 불일치

증상: 400 redirect_uri_mismatch
원인: Console에 URI 미등록 / http vs https
해결: 각 프로바이더 콘솔에서 URI 추가
교훈: OAuth 설정은 정확하게!

삽질 4: X-Forwarded-Proto 누락

증상: HTTPS인데 OAuth가 http로 리다이렉트
원인: Nginx에서 프록시 헤더 미전달
해결: proxy_set_header X-Forwarded-Proto $scheme; 추가
교훈: 프록시 환경에서는 헤더 전달 필수!

삽질 5: Nginx location 순서

증상: Python API로 안 가고 Spring Boot로 감
원인: /api/가 /api/v1/brainstorming/보다 먼저 매칭
해결: 구체적인 경로를 위에 배치
교훈: Nginx location 순서 중요!

💡 11. 배운 점

배포는 개발의 절반!

로컬 개발: 50%
배포 + 설정: 50%

코드 다 짜도 배포 못 하면 의미 없음!

환경 차이 인식

로컬: localhost로 다 됨
서버: 컨테이너 이름, 프록시 경로

처음부터 배포 환경 고려해서 설계하자!

보안은 기본

✅ HTTPS 필수
✅ 환경 변수로 비밀 관리
✅ 불필요한 포트 차단
✅ DB 외부 접근 차단

로그는 친구

# 문제 생기면 바로 로그 확인!
docker compose logs -f spring-boot
docker compose logs -f nginx
docker compose logs -f python-service

📊 12. 최종 결과

서비스 URL

https://idea-brainstorm.duckdns.org

기능 체크리스트

  • ✅ Google 로그인
  • ✅ Naver 로그인
  • ✅ Kakao 로그인
  • ✅ AI 브레인스토밍 (GPT-4)
  • ✅ 아이디어 저장/조회
  • ✅ 문의하기 시스템
  • ✅ 관리자 페이지

서버 상태

docker compose ps
NAME                STATUS              PORTS
brainstorm-mysql    Up (healthy)        3306/tcp
brainstorm-python   Up (healthy)        8000/tcp
brainstorm-spring   Up                  8080/tcp
brainstorm-nginx    Up                  80/tcp, 443/tcp

모든 컨테이너 정상 작동! ✅


🚀 13. 자주 쓰는 명령어 정리

서버 SSH 접속

ssh -i ~/Projects/PersonalProject/brainstorming/brainstorm-server-key.pem ubuntu@15.164.177.90

프로젝트 폴더 이동

cd ~/Braninstorming

컨테이너 관리

# 상태 확인
docker compose ps

# 로그 확인
docker compose logs -f spring-boot
docker compose logs -f python-service
docker compose logs -f nginx

# 재시작
docker compose restart

# 전체 재빌드
docker compose down
docker compose build
docker compose up -d

Git 작업

# 서버에서 최신 코드 받기
git pull origin main

# 로컬에서 푸시
git add .
git commit -m "커밋 메시지"
git push origin main

💬 14. 마무리

드디어... 첫 풀스택 배포 완료!!! 🎉🎉🎉

진짜 힘들었다... 😭

특히 힘들었던 것:

  • EC2 메모리 부족으로 서버 먹통
  • OAuth redirect_uri 삽질 (http vs https)
  • Nginx 프록시 설정 순서 문제
  • 각 프로바이더 콘솔 설정 찾아다니기

뿌듯한 것:

  • 진짜 인터넷에서 접속 가능한 서비스!
  • HTTPS 적용 완료 (자물쇠 뜸!)
  • OAuth 3개 다 작동!
  • AI 브레인스토밍도 정상 작동!
https://idea-brainstorm.duckdns.org

이 URL로 누구나 접속할 수 있다는 게 신기함! ✨

다음 목표

  • SSL 인증서 자동 갱신 (cron job)
  • 모니터링 설정 (서버 상태 체크)
  • CI/CD 파이프라인 (GitHub Actions)
  • 비용 최적화 (필요할 때만 서버 켜기?)

작성일: 2024-12-04
개발 시간: 약 8시간 (삽질 포함 😅)
GitHub: https://github.com/magui-dev/Brainstorming

#AWS #EC2 #Docker #Nginx #HTTPS #LetsEncrypt #OAuth #SpringBoot #Python #배포 #DevOps

🐳 Docker + AWS 배포 완전 가이드 (초보자도 OK!)

🎯 오늘의 목표

전체 시스템을 Docker로 컨테이너화하고 AWS EC2에 배포하기!


✨ 완성 후 모습

  • ✅ Spring Boot + Python + MySQL 한 번에 실행
  • docker-compose up 명령어 하나로 끝
  • ✅ AWS EC2에서 실제 서비스 운영
  • ✅ 환경 변수로 민감 정보 관리
  • ✅ 데이터 영구 저장 (볼륨)

결과: 진짜 서비스 배포 완료! 🚀


📚 Docker가 뭔가요?

간단하게 말하면

"내 컴퓨터에서는 되는데 서버에서는 안 돼요!" 문제 해결사

비유로 설명

전통 방식 (너무 복잡함):

1. 서버에 Java 17 설치
2. 서버에 Python 3.11 설치
3. 서버에 MySQL 설치
4. 환경 변수 100개 설정
5. 라이브러리 하나씩 설치
6. 버전 충돌 해결...
7. 포기 😭

Docker 방식 (간단함):

1. docker-compose up
2. 끝! 🎉

핵심 용어 정리

용어 설명 쉬운 비유
Image 실행 가능한 패키지 설치 파일 (.exe)
Container 실행 중인 Image 실행 중인 프로그램
Dockerfile Image 만드는 레시피 요리 레시피
docker-compose 여러 Container 동시 실행 세트 메뉴 주문
Volume 데이터 영구 저장 공간 외장 하드

우리가 만들 구조

┌─────────────────────────────────────────┐
│         docker-compose.yml              │
│      (전체 오케스트레이션)               │
└─────────┬───────────┬───────────────────┘
          │           │
    ┌─────▼─────┐ ┌──▼──────────┐ ┌──────────┐
    │  MySQL    │ │ Spring Boot │ │  Python  │
    │ Container │ │  Container  │ │Container │
    │  (3306)   │ │   (8080)    │ │  (8000)  │
    └───────────┘ └─────────────┘ └──────────┘
         │              │               │
    ┌────▼──────────────▼───────────────▼────┐
    │      brainstorm-network (Docker)        │
    └─────────────────────────────────────────┘

3개 컨테이너가 하나의 네트워크에서 서로 통신!


🛠️ 1. Dockerfile 작성 (Spring Boot)

왜 Dockerfile이 필요한가요?

Docker Image를 만드는 설명서!

"어떻게 내 앱을 실행할 건지"를 Docker에게 알려주는 파일

Spring Boot Dockerfile

경로: 프로젝트 루트/Dockerfile

# ==========================================
# 1단계: 빌드 (Gradle로 JAR 파일 생성)
# ==========================================
FROM gradle:8.5-jdk17 AS builder

WORKDIR /app

# Gradle 파일 먼저 복사 (캐싱 최적화)
COPY build.gradle settings.gradle ./
COPY gradle ./gradle

# 소스 코드 복사
COPY src ./src

# JAR 빌드 (테스트 스킵해서 시간 단축)
RUN gradle build -x test --no-daemon

# ==========================================
# 2단계: 실행 (가벼운 JRE 이미지)
# ==========================================
FROM openjdk:17-jdk-slim

WORKDIR /app

# 1단계에서 빌드한 JAR만 복사
COPY --from=builder /app/build/libs/*.jar app.jar

# 포트 노출
EXPOSE 8080

# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]

각 줄 상세 설명

FROM gradle:8.5-jdk17 AS builder
  • FROM: 베이스 이미지 선택
  • gradle:8.5-jdk17: Gradle 8.5 + Java 17 포함
  • AS builder: 이 단계를 "builder"라고 이름 붙임
WORKDIR /app
  • 컨테이너 안에서 /app 폴더를 작업 공간으로 설정
  • 이후 모든 명령어는 이 폴더에서 실행됨
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
  • Gradle 설정 파일을 먼저 복사
  • 왜? 소스 코드 변경돼도 이 부분은 캐시 재사용 가능!
  • 빌드 시간 엄청 단축됨 💨
RUN gradle build -x test --no-daemon
  • JAR 파일 빌드
  • -x test: 테스트 스킵 (시간 절약)
  • --no-daemon: Gradle 데몬 비활성화 (메모리 절약)
FROM openjdk:17-jdk-slim
  • 새로운 단계 시작!
  • slim: 최소한의 JRE만 포함 (이미지 크기 감소)
  • 빌드 도구(Gradle)는 버림 → 이미지 가벼워짐!
COPY --from=builder /app/build/libs/*.jar app.jar
  • --from=builder: 1단계에서 만든 JAR 파일만 가져옴
  • 빌드 도구, 소스 코드 등은 안 가져옴
  • 멀티 스테이지 빌드의 핵심!
ENTRYPOINT ["java", "-jar", "app.jar"]
  • 컨테이너 시작 시 실행할 명령어
  • CMD와 차이: 덮어쓰기 불가 (더 안전)

💡 멀티 스테이지 빌드의 장점

일반 빌드:
- Gradle (100MB)
- 소스 코드 (50MB)
- 빌드된 JAR (30MB)
───────────────────
총 이미지 크기: 180MB

멀티 스테이지 빌드:
- JRE (70MB)
- 빌드된 JAR (30MB)
───────────────────
총 이미지 크기: 100MB

45% 감소! 🎉


🐍 2. Dockerfile 작성 (Python FastAPI)

경로: python-service/Dockerfile

# Python 3.11 슬림 이미지
FROM python:3.11-slim

WORKDIR /app

# 시스템 패키지 업데이트 및 빌드 도구 설치
RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    && rm -rf /var/lib/apt/lists/*

# requirements.txt 먼저 복사 (캐싱 최적화)
COPY requirements.txt .

# Python 패키지 설치
RUN pip install --no-cache-dir -r requirements.txt

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 8000

# 실행 명령어
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

각 줄 상세 설명

FROM python:3.11-slim
  • Python 3.11 최소 버전 이미지
  • slim: 불필요한 패키지 제거된 버전
RUN apt-get update && apt-get install -y build-essential curl
  • build-essential: C 컴파일러 등 빌드 도구
  • 일부 Python 패키지(numpy 등)가 C로 작성되어 필요
  • curl: 헬스체크용
&& rm -rf /var/lib/apt/lists/*
  • apt 캐시 삭제
  • 이미지 크기 줄이기 위함
COPY requirements.txt .
  • requirements.txt만 먼저 복사
  • 소스 코드 변경돼도 pip install 캐시 재사용!
RUN pip install --no-cache-dir -r requirements.txt
  • --no-cache-dir: pip 캐시 안 남김 (이미지 크기 감소)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
  • --host 0.0.0.0: 매우 중요!
  • 기본값 127.0.0.1은 컨테이너 내부만 접근 가능
  • 0.0.0.0으로 해야 외부에서 접근 가능!

⚠️ 삽질 1: 127.0.0.1 vs 0.0.0.0

문제:

curl http://localhost:8000
# curl: (7) Failed to connect

원인:

# ❌ 이렇게 하면 외부 접근 불가
uvicorn.run("main:app", host="127.0.0.1")

# ✅ 이렇게 해야 함
uvicorn.run("main:app", host="0.0.0.0")

Docker 컨테이너는 격리된 환경!
127.0.0.1은 컨테이너 내부만 의미함!


🎼 3. docker-compose.yml 작성 (핵심!)

경로: 프로젝트 루트/docker-compose.yml

version: '3.8'

services:
  # ==========================================
  # MySQL 데이터베이스
  # ==========================================
  mysql:
    image: mysql:8.0
    container_name: brainstorm-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: brainstorm
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - brainstorm-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ==========================================
  # Python FastAPI (브레인스토밍 엔진)
  # ==========================================
  python-service:
    build:
      context: ./python-service
      dockerfile: Dockerfile
    container_name: brainstorm-python
    restart: always
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - LLM_MODEL=${LLM_MODEL:-gpt-4o}
      - EMBEDDING_MODEL=${EMBEDDING_MODEL:-text-embedding-3-large}
    ports:
      - "8000:8000"
    networks:
      - brainstorm-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # ==========================================
  # Spring Boot (메인 백엔드)
  # ==========================================
  spring-boot:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: brainstorm-spring
    restart: always
    depends_on:
      mysql:
        condition: service_healthy
      python-service:
        condition: service_healthy
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/brainstorm?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
      - SPRING_DATASOURCE_USERNAME=${MYSQL_USER}
      - SPRING_DATASOURCE_PASSWORD=${MYSQL_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
      - KAKAO_CLIENT_ID=${KAKAO_CLIENT_ID}
      - KAKAO_CLIENT_SECRET=${KAKAO_CLIENT_SECRET}
      - NAVER_CLIENT_ID=${NAVER_CLIENT_ID}
      - NAVER_CLIENT_SECRET=${NAVER_CLIENT_SECRET}
      - PYTHON_API_URL=http://python-service:8000
    ports:
      - "8080:8080"
    networks:
      - brainstorm-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

# ==========================================
# 볼륨 (데이터 영구 저장)
# ==========================================
volumes:
  mysql-data:
    driver: local

# ==========================================
# 네트워크 (컨테이너 간 통신)
# ==========================================
networks:
  brainstorm-network:
    driver: bridge

핵심 포인트 설명

1. restart: always

restart: always
  • 컨테이너 종료되면 자동 재시작
  • 서버 재부팅해도 자동으로 다시 실행됨!
  • 운영 환경 필수!

2. depends_on (중요! 🔥)

depends_on:
  mysql:
    condition: service_healthy
  • MySQL이 완전히 준비될 때까지 대기
  • condition: service_healthy: healthcheck 성공해야 시작
  • 순서: MySQL → Python → Spring Boot

왜 필요한가?

없으면:
MySQL 시작 중...
Spring Boot 시작! → MySQL 연결 시도 → 실패! 💥

있으면:
MySQL 시작 중...
MySQL healthcheck 성공!
Spring Boot 시작! → MySQL 연결 성공 ✅

3. healthcheck (생명줄!)

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  interval: 10s      # 10초마다 체크
  timeout: 5s        # 5초 안에 응답 없으면 실패
  retries: 5         # 5번 실패하면 unhealthy
  • 컨테이너가 진짜 준비됐는지 확인
  • 프로세스 실행 != 서비스 준비 완료

예시:

MySQL 프로세스 시작: 1초
MySQL 완전 준비: 10초

healthcheck 없으면:
→ 1초 후 Spring Boot 시작
→ MySQL 연결 실패 💥

healthcheck 있으면:
→ 10초 후 MySQL healthy
→ Spring Boot 시작
→ 연결 성공 ✅

4. environment (환경 변수)

environment:
  - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/...
  • mysql컨테이너 이름
  • Docker 네트워크에서 자동으로 IP 해석
  • localhost 쓰면 안 됨! (컨테이너 자기 자신 의미)

네트워크 매직:

Spring Boot 컨테이너에서:
jdbc:mysql://mysql:3306/brainstorm
              └─ 이름

Docker가 자동으로:
jdbc:mysql://172.18.0.2:3306/brainstorm
              └─ MySQL 컨테이너 IP

5. volumes (데이터 영구 저장)

volumes:
  - mysql-data:/var/lib/mysql
  • MySQL 데이터를 호스트에 저장
  • 컨테이너 삭제해도 데이터 유지!

비유:

volumes 없으면:
컨테이너 = 노트북 RAM
전원 끄면 다 날아감 💥

volumes 있으면:
컨테이너 = 노트북 RAM
volumes = 외장 하드
전원 끄면 RAM은 날아가지만 하드는 남음 ✅

6. networks (컨테이너 통신)

networks:
  - brainstorm-network
  • 모든 컨테이너가 같은 네트워크에 연결
  • 이름으로 서로 찾을 수 있음
  • 외부 네트워크와 격리됨 (보안!)

네트워크 구조:

┌─────────────────────────────────────────┐
│     brainstorm-network (내부)            │
│  ┌────────┐  ┌───────────┐  ┌────────┐ │
│  │ MySQL  │  │  Spring   │  │ Python │ │
│  │  3306  │◄─┤   8080    │◄─┤  8000  │ │
│  └────────┘  └───────────┘  └────────┘ │
│       │            │             │      │
└───────┼────────────┼─────────────┼──────┘
        │            │             │
        └────────────▼─────────────┘
            호스트 포트 매핑
         (3306, 8080, 8000)

🔐 4. 환경 변수 파일 (.env)

경로: 프로젝트 루트/.env

# ==========================================
# MySQL 설정
# ==========================================
MYSQL_ROOT_PASSWORD=your_super_secret_root_password
MYSQL_USER=brainstorm
MYSQL_PASSWORD=your_mysql_password_here

# ==========================================
# JWT 토큰
# ==========================================
JWT_SECRET=your_jwt_secret_at_least_32_characters_long_for_hs256

# ==========================================
# OAuth 2.0 (소셜 로그인)
# ==========================================
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

KAKAO_CLIENT_ID=your_kakao_client_id
KAKAO_CLIENT_SECRET=your_kakao_client_secret

NAVER_CLIENT_ID=your_naver_client_id
NAVER_CLIENT_SECRET=your_naver_client_secret

# ==========================================
# OpenAI API
# ==========================================
OPENAI_API_KEY=sk-your_openai_api_key_here
LLM_MODEL=gpt-4o
EMBEDDING_MODEL=text-embedding-3-large

⚠️ 매우 중요!

# .gitignore에 반드시 추가!
echo ".env" >> .gitignore

# 확인
cat .gitignore | grep .env

절대 Git에 올리면 안 됨! 🚨

💡 환경 변수 사용법

docker-compose.yml에서:

environment:
  - MYSQL_PASSWORD=${MYSQL_PASSWORD}
  #                  └─ .env에서 자동으로 읽어옴

기본값 설정:

environment:
  - LLM_MODEL=${LLM_MODEL:-gpt-4o}
  #                        └─ 없으면 gpt-4o 사용

🧪 5. 로컬 테스트 (단계별)

Step 1: Docker 설치 확인

# Docker 버전 확인
docker --version
# Docker version 24.0.7

# Docker Compose 버전 확인
docker compose version
# Docker Compose version v2.21.0

버전이 안 나오면?

Step 2: 프로젝트 준비

# 1. .env 파일 생성
cp .env.example .env  # 템플릿 있으면
nano .env             # 없으면 직접 생성

# 2. .env 파일 확인
cat .env

# 3. .gitignore 확인
cat .gitignore | grep .env

Step 3: 빌드 (처음 한 번만)

# 프로젝트 루트에서
docker compose build

# 또는 특정 서비스만
docker compose build spring-boot
docker compose build python-service

예상 시간:

  • Spring Boot: 2-3분
  • Python: 1-2분
  • 총: 3-5분

빌드 로그 예시:

[+] Building 187.3s (22/22) FINISHED
 => [spring-boot builder 1/6] FROM docker.io/library/gradle:8.5-jdk17
 => [spring-boot builder 2/6] WORKDIR /app
 => [spring-boot builder 3/6] COPY build.gradle settings.gradle ./
 => [spring-boot builder 4/6] COPY gradle ./gradle
 => [spring-boot builder 5/6] COPY src ./src
 => [spring-boot builder 6/6] RUN gradle build -x test --no-daemon
 => [spring-boot stage-1 1/2] FROM docker.io/library/openjdk:17-jdk-slim
 => [spring-boot stage-1 2/2] COPY --from=builder /app/build/libs/*.jar app.jar
 => => exporting to image
 => => naming to docker.io/library/brainstorming-platform-spring-boot

Step 4: 실행

# 포그라운드 실행 (로그 보임)
docker compose up

# 백그라운드 실행 (추천!)
docker compose up -d

실행 순서:

1. MySQL 시작...
2. MySQL healthcheck...
3. MySQL healthy ✅
4. Python 시작...
5. Python healthcheck...
6. Python healthy ✅
7. Spring Boot 시작...
8. Spring Boot healthy ✅
9. 전체 완료! 🎉

로그 예시:

[+] Running 3/3
 ✔ Container brainstorm-mysql   Healthy    12.3s
 ✔ Container brainstorm-python  Healthy    15.6s
 ✔ Container brainstorm-spring  Started    18.2s

Step 5: 상태 확인

# 컨테이너 상태 확인
docker compose ps

# 결과:
NAME                  STATUS         PORTS
brainstorm-mysql      Up (healthy)   0.0.0.0:3306->3306/tcp
brainstorm-python     Up (healthy)   0.0.0.0:8000->8000/tcp
brainstorm-spring     Up (healthy)   0.0.0.0:8080->8080/tcp

"Up (healthy)" 이면 완벽! ✅

Step 6: 로그 확인

# 전체 로그
docker compose logs

# 특정 서비스만
docker compose logs spring-boot
docker compose logs python-service
docker compose logs mysql

# 실시간 로그 (Ctrl+C로 종료)
docker compose logs -f

# 최근 100줄만
docker compose logs --tail=100

Step 7: 테스트

# 1. Python API 헬스체크
curl http://localhost:8000/health

# 응답:
# {
#   "status": "healthy",
#   "service": "Brainstorming API",
#   "openai_key_set": true
# }

# 2. Spring Boot 헬스체크
curl http://localhost:8080/actuator/health

# 응답:
# {"status":"UP"}

# 3. 브라우저 테스트
open http://localhost:8080
# 또는 크롬에서 직접 접속

Step 8: 종료

# 중지 (컨테이너 유지)
docker compose stop

# 재시작
docker compose start

# 중지 + 삭제 (데이터는 유지)
docker compose down

# 완전 초기화 (볼륨까지 삭제)
docker compose down -v

😵 6. 자주 발생하는 오류

오류 1: 포트 이미 사용 중

증상:

Error response from daemon: Ports are not available: 
exposing port TCP 0.0.0.0:8080 -> 0.0.0.0:0: 
listen tcp 0.0.0.0:8080: bind: address already in use

원인: 로컬에서 서비스 실행 중

해결 방법 1: 프로세스 종료

# 실행 중인 프로세스 찾기
lsof -i :8080  # Spring Boot
lsof -i :8000  # Python
lsof -i :3306  # MySQL

# 예시 출력:
# java    12345 user   123u  IPv6 0x... TCP *:8080 (LISTEN)

# 종료
kill -9 12345

해결 방법 2: 포트 변경

# docker-compose.yml
services:
  spring-boot:
    ports:
      - "8081:8080"  # 호스트:컨테이너
      #  └─ 8081로 변경

오류 2: MySQL 연결 실패

증상:

spring-boot | com.mysql.cj.jdbc.exceptions.CommunicationsException: 
Communications link failure

원인 1: MySQL이 준비 안 됨

# ✅ 해결: depends_on 추가 (이미 추가됨)
depends_on:
  mysql:
    condition: service_healthy

원인 2: 환경 변수 오타

# .env 파일 확인
cat .env | grep MYSQL

# docker-compose.yml과 비교
cat docker-compose.yml | grep MYSQL

원인 3: 네트워크 문제

# 네트워크 확인
docker network ls

# 컨테이너 네트워크 확인
docker inspect brainstorm-mysql | grep NetworkMode

오류 3: 빌드 실패

증상:

ERROR: failed to solve: process "/bin/sh -c gradle build" 
did not complete successfully: exit code: 1

해결 방법 1: 캐시 삭제 후 재빌드

# 캐시 없이 빌드
docker compose build --no-cache

# 또는 전체 정리 후
docker system prune -a
docker compose build

해결 방법 2: 로그 확인

# 빌드 로그 자세히 보기
docker compose build --progress=plain

오류 4: 메모리 부족

증상:

java.lang.OutOfMemoryError: Java heap space

해결: docker-compose.yml에 메모리 제한 추가

services:
  spring-boot:
    # ... 기존 설정
    deploy:
      resources:
        limits:
          memory: 1G       # 최대 1GB
        reservations:
          memory: 512M     # 최소 512MB
    environment:
      - JAVA_OPTS=-Xmx512m -Xms256m  # JVM 힙 크기 설정

오류 5: 데이터 볼륨 문제

증상: 데이터가 초기화됨

원인: docker compose down -v 실행

# ❌ 볼륨까지 삭제
docker compose down -v

# ✅ 볼륨 유지
docker compose down

볼륨 확인:

# 볼륨 목록
docker volume ls

# 특정 볼륨 상세 정보
docker volume inspect brainstorming-platform_mysql-data

☁️ 7. AWS EC2 배포 (단계별)

준비물

  • AWS 계정
  • 신용카드 (무료 티어 사용해도 필요)
  • 인내심 😅

Step 1: EC2 인스턴스 생성

1. AWS 콘솔 로그인

https://console.aws.amazon.com/

2. EC2 대시보드

서비스 → EC2 → "인스턴스 시작" 클릭

3. 인스턴스 설정

항목 설정값 설명
이름 brainstorm-server 알아보기 쉬운 이름
AMI Ubuntu Server 22.04 LTS 안정적인 최신 버전
인스턴스 유형 t3.medium 2 vCPU, 4GB RAM
키 페어 새로 생성 또는 기존 사용 중요! 잘 보관
스토리지 30GB gp3 기본 8GB는 부족함

4. 보안 그룹 설정 (매우 중요! 🔥)

"편집" 클릭 → 다음 규칙 추가:

유형 프로토콜 포트 범위 소스 설명
SSH TCP 22 내 IP 서버 접속용
HTTP TCP 80 0.0.0.0/0 웹 (나중에)
HTTPS TCP 443 0.0.0.0/0 웹 (나중에)
Custom TCP TCP 8080 0.0.0.0/0 Spring Boot
Custom TCP TCP 8000 0.0.0.0/0 Python API

5. 인스턴스 시작!

"인스턴스 시작" 버튼 클릭

6. 키 페어 다운로드

  • brainstorm-key.pem 다운로드
  • 안전한 곳에 보관!
  • 잃어버리면 서버 접속 불가! 🚨

Step 2: EC2 접속

1. 키 파일 권한 변경 (Mac/Linux)

chmod 400 brainstorm-key.pem

2. SSH 접속

# EC2 퍼블릭 IP 확인 (AWS 콘솔에서)
# 예: 13.125.123.45

# SSH 접속
ssh -i brainstorm-key.pem ubuntu@13.125.123.45

3. 접속 성공!

Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 6.2.0-1009-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

ubuntu@ip-172-31-12-34:~$

Step 3: Docker 설치

# 1. 시스템 업데이트
sudo apt update && sudo apt upgrade -y

# 2. Docker 설치 (공식 스크립트)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 3. 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker ubuntu

# 4. 재로그인 (권한 적용)
exit
ssh -i brainstorm-key.pem ubuntu@13.125.123.45

# 5. Docker 버전 확인
docker --version
# Docker version 24.0.7, build afdd53b

# 6. Docker Compose 설치
sudo apt install docker-compose-plugin -y

# 7. Docker Compose 버전 확인
docker compose version
# Docker Compose version v2.21.0

Step 4: 코드 배포

방법 1: Git Clone (권장)

# Git 설치
sudo apt install git -y

# 코드 클론
git clone https://github.com/your-username/brainstorming-platform.git
cd brainstorming-platform

방법 2: SCP 업로드

# 로컬에서 실행 (Mac/Linux)
scp -i brainstorm-key.pem -r ./brainstorming-platform ubuntu@13.125.123.45:~/

# Windows (PowerShell)
scp -i brainstorm-key.pem -r .\brainstorming-platform ubuntu@13.125.123.45:~/

Step 5: 환경 변수 설정

# .env 파일 생성
nano .env

# 내용 붙여넣기 (로컬 .env 복사)
# Ctrl + O (저장)
# Enter
# Ctrl + X (종료)

# 확인
cat .env

Step 6: 빌드 및 실행

# 빌드 (시간 걸림!)
docker compose build

# 실행
docker compose up -d

# 상태 확인
docker compose ps

# 로그 확인
docker compose logs -f

Step 7: 방화벽 설정 (Ubuntu)

# UFW 설치 확인
sudo apt install ufw -y

# 기본 정책
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 포트 허용
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP
sudo ufw allow 443/tcp   # HTTPS
sudo ufw allow 8080/tcp  # Spring Boot
sudo ufw allow 8000/tcp  # Python

# 방화벽 활성화
sudo ufw enable

# 상태 확인
sudo ufw status

# 결과:
# Status: active
# To                         Action      From
# --                         ------      ----
# 22/tcp                     ALLOW       Anywhere
# 80/tcp                     ALLOW       Anywhere
# 443/tcp                    ALLOW       Anywhere
# 8080/tcp                   ALLOW       Anywhere
# 8000/tcp                   ALLOW       Anywhere

Step 8: 테스트

# EC2 IP 확인
curl ifconfig.me
# 13.125.123.45

# 브라우저에서 접속
http://13.125.123.45:8080

# 또는 curl로
curl http://13.125.123.45:8080/actuator/health

접속되면 성공! 🎉


🔧 8. 유용한 명령어 모음

Docker 관리

# 컨테이너 목록
docker ps
docker ps -a  # 중지된 것도 포함

# 이미지 목록
docker images

# 로그 보기
docker logs brainstorm-spring
docker logs -f brainstorm-spring  # 실시간
docker logs --tail=100 brainstorm-spring  # 최근 100줄

# 컨테이너 내부 접속
docker exec -it brainstorm-spring bash
docker exec -it brainstorm-mysql mysql -u root -p

# 리소스 사용량
docker stats

# 디스크 사용량
docker system df

# 디스크 정리 (조심!)
docker system prune        # 사용 안 하는 것 정리
docker system prune -a     # 모든 이미지까지 삭제
docker volume prune        # 사용 안 하는 볼륨 삭제

Docker Compose 관리

# 빌드
docker compose build              # 전체
docker compose build spring-boot  # 특정 서비스만

# 실행
docker compose up            # 포그라운드
docker compose up -d         # 백그라운드
docker compose up --build    # 빌드 + 실행

# 중지/시작
docker compose stop          # 중지
docker compose start         # 시작
docker compose restart       # 재시작

# 삭제
docker compose down          # 컨테이너만 삭제 (볼륨 유지)
docker compose down -v       # 볼륨까지 삭제 (데이터 삭제!)

# 로그
docker compose logs
docker compose logs -f
docker compose logs --tail=100
docker compose logs spring-boot

# 상태
docker compose ps
docker compose top

MySQL 관리

# MySQL 컨테이너 접속
docker exec -it brainstorm-mysql bash

# MySQL 로그인
mysql -u root -p
# 또는
mysql -u brainstorm -p

# SQL 명령어
SHOW DATABASES;
USE brainstorm;
SHOW TABLES;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM ideas;
SELECT COUNT(*) FROM inquiries;

# 백업
docker exec brainstorm-mysql mysqldump -u root -p brainstorm > backup.sql

# 복원
docker exec -i brainstorm-mysql mysql -u root -p brainstorm < backup.sql

재배포 (코드 수정 후)

# 1. 코드 업데이트
git pull

# 2. 컨테이너 중지
docker compose down

# 3. 이미지 다시 빌드
docker compose build

# 4. 컨테이너 재시작
docker compose up -d

# 한 줄로 (편리!)
git pull && docker compose down && docker compose build && docker compose up -d

디버깅

# 컨테이너 상세 정보
docker inspect brainstorm-spring

# 네트워크 확인
docker network ls
docker network inspect brainstorming-platform_brainstorm-network

# 볼륨 확인
docker volume ls
docker volume inspect brainstorming-platform_mysql-data

# 헬스체크 상태
docker inspect --format='{{json .State.Health}}' brainstorm-spring | jq

# 환경 변수 확인
docker exec brainstorm-spring env

# 포트 확인
docker port brainstorm-spring

🐛 9. 트러블슈팅

문제 1: 컨테이너가 계속 재시작됨

확인:

docker compose ps

# 결과:
# brainstorm-spring  Restarting (1)  ...

원인 파악:

# 로그 확인
docker compose logs spring-boot

# 예시 오류:
# Error: Unable to connect to MySQL server

해결:

  1. MySQL 먼저 확인
  2. 환경 변수 확인
  3. healthcheck 시간 늘리기

문제 2: 디스크 공간 부족

확인:

df -h

# 결과:
# /dev/xvda1  30G  28G  2.0G  94%  /

해결:

# Docker 정리
docker system prune -a
docker volume prune

# 빌드 캐시 삭제
docker builder prune

# 로그 파일 정리
sudo journalctl --vacuum-time=3d

문제 3: 메모리 부족

확인:

free -h

# 결과:
#               total        used        free
# Mem:          3.8Gi       3.5Gi       300Mi

해결:

# 스왑 추가 (4GB)
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 영구 적용
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 확인
free -h

문제 4: OAuth 리다이렉트 실패

원인: 로컬 URL로 설정되어 있음

해결:

Google/Kakao/Naver 개발자 콘솔에서 리다이렉트 URL 수정

변경 전:
http://localhost:8080/login/oauth2/code/google

변경 후:
http://13.125.123.45:8080/login/oauth2/code/google
또는
https://yourdomain.com/login/oauth2/code/google

💡 10. 배운 점

Docker의 장점

  1. 환경 일관성

    • 로컬 = 개발 = 운영
    • "내 컴에서는 되는데?" 문제 해결
  2. 빠른 배포

    • docker compose up 한 줄로 끝
    • 설정 파일만 관리하면 됨
  3. 쉬운 롤백

    • 이미지 버전 관리
    • 문제 생기면 이전 버전으로 즉시 복구
  4. 리소스 효율

    • VM보다 가볍고 빠름
    • 동일 서버에 여러 환경 운영 가능

삽질하며 배운 것

  1. healthcheck의 중요성

    • 프로세스 시작 != 서비스 준비
    • MySQL 준비 완료까지 10초 걸림
  2. 네트워크 이해

    • localhost vs 0.0.0.0 vs 컨테이너 이름
    • Docker 네트워크는 격리된 환경
  3. 볼륨의 필요성

    • 컨테이너는 휘발성
    • 데이터는 반드시 볼륨에 저장
  4. 환경 변수 관리

    • 민감 정보는 .env로
    • Git에 절대 올리지 말기

🚀 11. 다음 단계

지금 완성된 것

  • ✅ Docker로 전체 시스템 컨테이너화
  • ✅ docker-compose로 한 번에 실행
  • ✅ AWS EC2에 배포 완료

앞으로 할 것

1. 도메인 + HTTPS

# 무료 도메인 + Let's Encrypt
- Freenom에서 도메인 받기
- Nginx 리버스 프록시 추가
- Certbot으로 SSL 인증서 발급

2. CI/CD 파이프라인

# GitHub Actions
- 코드 푸시 → 자동 빌드
- 테스트 실행
- Docker 이미지 생성
- EC2 자동 배포

3. 모니터링

# Grafana + Prometheus
- CPU/메모리 사용량 모니터링
- 로그 수집 및 분석
- 알림 설정 (서버 다운 시)

4. 백업 자동화

# 매일 자동 백업
- MySQL 데이터 백업
- S3에 업로드
- 주기적인 백업 테스트

📊 12. 최종 시스템 구조

┌─────────────────────────────────────────────────┐
│              AWS EC2 (Ubuntu 22.04)              │
│  ┌───────────────────────────────────────────┐  │
│  │         Docker Engine                     │  │
│  │  ┌──────────────────────────────────┐    │  │
│  │  │  brainstorm-network              │    │  │
│  │  │                                  │    │  │
│  │  │  ┌────────┐  ┌─────────────┐    │    │  │
│  │  │  │ MySQL  │  │ Spring Boot │    │    │  │
│  │  │  │ :3306  │◄─┤   :8080     │    │    │  │
│  │  │  └────┬───┘  └──────┬──────┘    │    │  │
│  │  │       │             │  ▲         │    │  │
│  │  │       │             │  │         │    │  │
│  │  │  ┌────▼─────────────▼──┘         │    │  │
│  │  │  │   Python FastAPI   │          │    │  │
│  │  │  │      :8000         │          │    │  │
│  │  │  └────────────────────┘          │    │  │
│  │  └──────────────────────────────────┘    │  │
│  │                                           │  │
│  │  Volumes:                                 │  │
│  │  └─ mysql-data (30GB)                    │  │
│  └───────────────────────────────────────────┘  │
│                                                  │
│  포트 매핑:                                      │
│  - 3306 → MySQL                                 │
│  - 8080 → Spring Boot                           │
│  - 8000 → Python FastAPI                        │
└─────────────────────────────────────────────────┘
                     ▲
                     │
              인터넷 접속
         http://13.125.123.45:8080

💬 13. 마무리

Docker... 처음엔 어려웠는데 막상 해보니 신기하다! 😮

특히 docker compose up 한 줄로 전체 시스템이 뜨는 게 정말 편함.

힘들었던 점:

  1. healthcheck 개념 이해
  2. 네트워크 설정 (localhost vs 0.0.0.0)
  3. 환경 변수 관리
  4. AWS 보안 그룹 설정

뿌듯한 점:

  1. 전체 시스템 컨테이너화 완료!
  2. AWS에 실제 배포 성공!
  3. 누구든 git clone + docker compose up으로 실행 가능
  4. 재배포도 한 줄로 가능!

이제 진짜 서비스 운영할 수 있다!

다음 목표: CI/CD 파이프라인 구축 → 코드 푸시만 하면 자동 배포!


작성일: 2024-12-04
소요 시간: 약 3시간
GitHub: [저장소 링크]

#Docker #DockerCompose #AWS #EC2 #배포 #컨테이너 #DevOps


'[그래서 일단 프로젝트] > [개인프로젝트-IdeaMaker]' 카테고리의 다른 글

기록 8. 전체 시스템 통합 완성 (0)
기록 7. OAuth 2.0 / JWT 구현 (0)
기록 6. 컨트롤러 계층 완성 (0)

🧠 전체 시스템 통합 완성 - 브레인스토밍부터 저장까지

🎯 오늘의 목표

AI 브레인스토밍 → 아이디어 생성 → DB 저장 → 다시보기 전체 플로우 완성!


✨ 완성된 것들

  • ✅ H2 → MySQL 데이터베이스 전환
  • ✅ JWT 토큰에 role 정보 추가 (관리자 구분)
  • ✅ 브레인스토밍 전체 프로세스 (Q1 → Q2 → Q3 → 아이디어 생성)
  • ✅ 아이디어 저장 기능
  • ✅ 저장된 아이디어 상세보기 모달
  • ✅ Ephemeral RAG 자동 삭제
  • ✅ 문의하기 시스템 (사용자 + 관리자)

결과: 이제 진짜 쓸 수 있는 서비스가 됐다! 🎉


📚 시스템 구조 전체 그림

┌─────────────────────────────────────────────────────────┐
│                    사용자 브라우저                         │
│  (index.html, brainstorm.html, inquiry.html)            │
└──────────────┬──────────────────┬───────────────────────┘
               │                  │
      OAuth 로그인           브레인스토밍
               │                  │
               ▼                  ▼
┌──────────────────────┐  ┌──────────────────────┐
│  Spring Boot 8080    │  │  Python FastAPI 8000 │
│  - JWT 인증          │  │  - LLM (GPT-4)       │
│  - User CRUD         │  │  - Ephemeral RAG     │
│  - Idea CRUD         │  │  - 아이디어 생성      │
│  - Inquiry CRUD      │  │  - SWOT 분석         │
└──────┬───────────────┘  └──────────────────────┘
       │
       ▼
┌──────────────────────┐
│   MySQL 3306         │
│  - users             │
│  - ideas             │
│  - inquiries         │
└──────────────────────┘

🔥 1. H2 → MySQL 전환 (데이터 영구 보존)

문제 발견

H2 인메모리 DB 쓰다가... 서버 재시작하면 데이터 다 날아감 😱

서버 재시작
↓
"어? 내가 만든 아이디어 어디갔지?"
↓
전부 사라짐... 💥

해결: MySQL 전환

1단계: application.yaml 수정

spring:
  datasource:
    # ❌ H2 (휘발성)
    # url: jdbc:h2:mem:brainstorm
    # driver-class-name: org.h2.Driver

    # ✅ MySQL (영구 저장)
    url: jdbc:mysql://localhost:3306/brainstorm?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: ${MYSQL_PASSWORD}

  jpa:
    hibernate:
      ddl-auto: update  # ⚠️ 중요! create-drop → update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect

핵심 포인트:

  • ddl-auto: create-drop → 서버 끌 때 테이블 삭제
  • ddl-auto: update → 테이블 유지, 스키마만 업데이트

2단계: MySQL 데이터베이스 생성

CREATE DATABASE brainstorm 
  CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;

utf8mb4 → 이모지 저장 가능! 🎉

3단계: 테스트

# Spring Boot 실행
./gradlew bootRun

# MySQL 접속
mysql -u root -p brainstorm

# 테이블 확인
SHOW TABLES;
+---------------------+
| Tables_in_brainstorm|
+---------------------+
| users               |
| ideas               |
| inquiries           |
+---------------------+

완벽! 테이블 자동 생성됨! ✅


👑 2. 관리자 계정 자동 생성

문제: 관리자가 없다

문의 답변할 관리자 계정이 필요한데... 수동으로 만들기 귀찮음 😅

해결: CommandLineRunner

DataInitializer.java

@Slf4j
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Value("${admin.email:admin@brainstorm.com}")
    private String adminEmail;

    @Value("${admin.password:admin1234}")
    private String adminPassword;

    @Override
    public void run(String... args) {
        log.info("🔍 관리자 계정 확인 중...");

        // ADMIN 권한 가진 사용자 있는지 확인
        boolean adminExists = userRepository
            .existsByRole(MyRole.ADMIN);

        if (!adminExists) {
            log.info("✨ 관리자 계정이 없습니다. 생성합니다...");

            User admin = User.builder()
                .email(adminEmail)
                .username("관리자")
                .provider(LoginProvider.LOCAL)
                .providerId("admin")
                .role(MyRole.ADMIN)
                .build();

            userRepository.save(admin);
            log.info("✅ 관리자 계정 생성 완료!");
            log.info("   📧 이메일: {}", adminEmail);
        } else {
            log.info("✅ 관리자 계정 이미 존재합니다.");
        }
    }
}

실행 로그:

🔍 관리자 계정 확인 중...
✨ 관리자 계정이 없습니다. 생성합니다...
✅ 관리자 계정 생성 완료!
   📧 이메일: admin@brainstorm.com

이제 서버 시작하면 자동으로 관리자 생성됨! 👍


🔑 3. JWT 토큰에 role 정보 추가

문제 발견

관리자 페이지 접속 시도

❌ "관리자 권한이 필요합니다"

어? 나 관리자인데? 🤔

원인 파악

JWT 토큰 디코딩해보니:

{
  "sub": "2",           // ✅ userId 있음
  "iat": 1701234567,    // ✅ 발행 시간 있음
  "exp": 1701241767     // ✅ 만료 시간 있음
  // ❌ role 정보 없음!
}

role이 없으니까 권한 확인 불가! 💥

해결: JWT에 role 추가

JwtTokenProvider.java 수정

public String createToken(User user) {
    log.info("🔐 JWT 토큰 생성 시작 - userId: {}", user.getUserId());

    Date now = new Date();
    Date expiration = new Date(now.getTime() + jwtProperties.getExpiration());

    String token = Jwts.builder()
        .subject(String.valueOf(user.getUserId()))
        .claim("role", user.getRole().name())  // ✅ role 추가!
        .issuedAt(now)
        .expiration(expiration)
        .signWith(getSignKey())
        .compact();

    log.info("✅ JWT 토큰 생성 완료 - role: {}", user.getRole());
    return token;
}

// role 추출 메서드 추가
public MyRole getRoleFromToken(String token) {
    log.info("🔍 JWT 토큰에서 role 추출");

    Claims claims = Jwts.parser()
        .verifyWith(getSignKey())
        .build()
        .parseSignedClaims(token)
        .getPayload();

    String roleString = claims.get("role", String.class);
    MyRole role = MyRole.valueOf(roleString);

    log.info("✅ role 추출 성공: {}", role);
    return role;
}

이제 JWT 토큰:

{
  "sub": "2",
  "role": "ADMIN",      // ✅ 추가됨!
  "iat": 1701234567,
  "exp": 1701241767
}

UserResponseDto도 수정

@Getter
@AllArgsConstructor
public class UserResponseDto {
    private Long userId;
    private String email;
    private String username;
    private MyRole role;  // ✅ 추가!

    public static UserResponseDto from(User user) {
        return new UserResponseDto(
            user.getUserId(),
            user.getEmail(),
            user.getUsername(),
            user.getRole()  // ✅ 매핑!
        );
    }
}

⚠️ 삽질 1: 기존 토큰 문제

새 코드 배포했는데도 여전히 오류 발생...

원인: localStorage에 예전 토큰이 남아있음!

해결:

// F12 Console에서
localStorage.clear();
// 다시 로그인 → 새 토큰 발급

🧠 4. 브레인스토밍 전체 프로세스

시스템 구조

프론트엔드 (brainstorm.html + brainstorm.js)
         ↓
Python FastAPI (브레인스토밍 엔진)
         ↓
OpenAI GPT-4 (LLM)
         ↓
ChromaDB (Ephemeral RAG)

Step 1: 세션 시작

JavaScript:

async function startSession() {
    const response = await fetch(`${API_BASE_URL}/session`, {
        method: 'POST'
    });

    const data = await response.json();
    sessionId = data.session_id;

    console.log('✅ 세션 시작:', sessionId);
}

Python 로그:

🧹 오래된 Ephemeral 세션 폴더 청소 시작...
   - 삭제: session_abc123... (생성 후 6.3분 경과)
✅ 1개의 오래된 빈 폴더 삭제됨
✅ 새 세션 생성: session_def456

5분 이상 된 세션은 자동으로 청소됨! 깔끔! 🧹

Step 2: Q1 - 목적 입력

사용자 입력:

"유튜브 컨텐츠 아이디어"

Python:

@router.post("/purpose")
async def submit_purpose(request: PurposeRequest):
    session_manager.update_session(request.session_id, {
        'q1_purpose': request.purpose
    })

    logger.info(f"✅ 목적 입력 완료")
    logger.info(f"   📝 목적: {request.purpose}")

Step 3: Q2 - 워밍업 질문 생성

LLM Prompt:

사용자가 "유튜브 컨텐츠 아이디어"에 대한 아이디어를 생성하려고 합니다.

목표: 사용자의 직군/상황에 맞는 구체적인 워밍업 질문 2-3개 생성

예시:
- 누군가에게 자랑하고 싶은 결과물이라면 누구인가요?
- 어떤 감정을 느끼게 하고 싶나요?

GPT-4 응답:

- 구독자들이 가장 많이 요청한 주제는 무엇인가요?
- 촬영이 가장 재미있었던 순간은 언제인가요?
- 10년 후에도 남고 싶은 영상은 어떤 것인가요?

UI 표시:

data.questions.forEach((q, index) => {
    addMessage('ai', `${index + 1}. ${q}`, false);
});

Step 4: Q3 - 자유연상 (30초)

UI 구조:

<div class="association-tags-display">
    <!-- 태그가 여기 쌓임 -->
    <span class="association-tag">
        브이로그 
        <button onclick="removeAssociation('브이로그')">×</button>
    </span>
    <span class="association-tag">일상 <button>×</button></span>
    <span class="association-tag">먹방 <button>×</button></span>
    <!-- ... -->
</div>

<input 
    type="text" 
    placeholder="키워드 입력 후 Enter..."
    onkeypress="handleAssociationEnter(event)"
>

<button 
    id="submitAssociationsBtn" 
    onclick="submitAssociations()"
    disabled
>
    🎨 생성 (0개)
</button>

동적 버튼 활성화:

function updateAssociationButton() {
    const count = associations.length;

    if (count < 5) {
        message = '지금부터 떠오르는 무엇이든 자유롭게 많이 적어주세요.';
        showButton = false;
    } else if (count >= 5 && count <= 9) {
        message = '😊 좋아요! 조금만 더 입력해볼까요?';
        showButton = false;
    } else if (count >= 10) {
        message = '🎉 많이 입력했네요~! 준비되셨으면 생성 버튼을 눌러주세요';
        showButton = true;
    }

    button.innerHTML = `🎨 생성 (${count}개)`;
    button.disabled = !showButton;
}

10개 이상 입력해야 버튼 활성화! UX 디테일! 👍

Step 5: 아이디어 생성 (핵심!)

Python - Ephemeral RAG 활용:

@router.get("/ideas/{session_id}")
async def generate_ideas(session_id: str):
    # 1. Ephemeral RAG 초기화
    ephemeral_rag = EphemeralRAG(
        session_id=session_id,
        collection_name=session['chroma_collection'],
        chroma_client=chroma_client
    )

    # 2. Q3 키워드 중 유사도 높은 것 추출
    keywords_data = ephemeral_rag.extract_keywords_by_similarity(
        purpose=purpose,
        top_k=5
    )

    # 예: ['브이로그', '일상', '먹방', '리뷰', '챌린지']

    # 3. 영구 RAG에서 브레인스토밍 기법 검색
    results = permanent_collection.query(
        query_embeddings=[purpose_embedding],
        n_results=3
    )

    # 예: SCAMPER, 마인드맵, 스타버스팅 기법

    # 4. LLM에게 아이디어 생성 요청
    prompt = f"""
    목적: "{purpose}"
    연상 키워드: {extracted_keywords}
    브레인스토밍 기법: {rag_context}

    규칙:
    - 허구 데이터 절대 금지
    - 현실적 실행 가능성
    - 구체적인 행동 중심
    - 2-3개 아이디어 생성

    형식:
    아이디어 1: [제목]
    💡 핵심 문제: ...
    ✨ 개선 방안: ...
    🎯 기대 효과: ...
    📊 분석 결과: (SWOT)
    """

    ideas_text = call_llm_with_retry(
        client=openai_client,
        model="gpt-4o",
        messages=[...],
        temperature=0.7
    )

GPT-4 응답 예시:

아이디어 1: "10분 브이로그 - 일상의 작은 순간들"

💡 핵심 문제:
유튜버들이 긴 영상 편집에 시간을 많이 소비하고, 
구독자들은 부담 없이 볼 수 있는 짧은 콘텐츠를 선호

✨ 개선 방안:
10분 이내 브이로그 형식으로 일상의 작은 순간들을 담는다.
휴대폰 하나로 촬영, 간단한 자막만 추가

🎯 기대 효과:
- 편집 시간 50% 단축
- 업로드 주기 2배 증가
- 구독자 이탈률 감소

🎨 발상 기법:
SCAMPER의 축소(Minimize) 기법 적용 - 
긴 영상을 10분으로 줄이고 핵심만 남김

📊 분석 결과:
• 강점:
  - 편집 부담 적음
  - 업로드 주기 짧음
• 약점:
  - 수익화 어려움 (짧은 영상)
  - 광고 삽입 제한
• 기회:
  - 쇼츠 → 본 영상 전환 유도
  - 시리즈물 기획 가능
• 위협:
  - 경쟁자 많음
  - 차별화 필요

파싱 로직:

ideas = []
current_idea = None

for line in ideas_text.split('\n'):
    if re.match(r'^아이디어\s+\d+:', line):
        if current_idea:
            ideas.append(current_idea)

        title = line.split(':', 1)[1].strip()
        current_idea = {
            'title': title,
            'description': '',
            'analysis': ''
        }

    elif '💡 핵심 문제' in line:
        current_section = 'problem'
    # ... 섹션별 파싱

프론트엔드 표시:

function displayIdeas(ideas) {
    ideas.forEach((idea, index) => {
        let ideaHtml = `
            <div class="idea-result" onclick="toggleIdea(${index})">
                <h3>
                    <span>💡 아이디어 ${index + 1}: ${idea.title}</span>
                    <span class="idea-toggle">▶</span>
                </h3>
                <div class="idea-content" id="content-${index}">
                    <div class="idea-description">
                        ${idea.description.replace(/\n/g, '<br>')}
                    </div>
                    <div class="idea-analysis">
                        <h4>📊 분석 결과</h4>
                        ${idea.analysis.replace(/\n/g, '<br>')}
                    </div>
                </div>
            </div>
        `;
        chatBox.insertAdjacentHTML('beforeend', ideaHtml);
    });

    // ✅ 저장 버튼 추가!
    const saveButtonHtml = `
        <div style="text-align: center; margin: 3rem 0;">
            <button onclick="saveIdeas()">
                💾 아이디어 저장하기
            </button>
        </div>
    `;
    chatBox.insertAdjacentHTML('beforeend', saveButtonHtml);

    window.generatedIdeas = ideas;  // 전역 저장
}

💾 5. 아이디어 저장 기능

JavaScript 구현

async function saveIdeas() {
    // 1. 로그인 확인
    const token = localStorage.getItem('token');
    if (!token) {
        alert('로그인이 필요합니다.');
        location.href = 'index.html';
        return;
    }

    // 2. 사용자 정보 가져오기
    const userResponse = await fetch('http://localhost:8080/api/auth/me', {
        headers: { 'Authorization': `Bearer ${token}` }
    });
    const currentUser = await userResponse.json();

    // 3. 각 아이디어 저장
    const savePromises = window.generatedIdeas.map(async (idea) => {
        const ideaData = {
            userId: currentUser.userId,
            title: idea.title,
            content: JSON.stringify({
                description: idea.description,
                analysis: idea.analysis,
                generatedAt: new Date().toISOString()
            }),
            purpose: sessionId || 'brainstorm_session'
        };

        const response = await fetch('http://localhost:8080/api/ideas', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(ideaData)
        });

        return await response.json();
    });

    await Promise.all(savePromises);

    // 4. Ephemeral RAG 세션 삭제
    try {
        const deleteResponse = await fetch(
            `${API_BASE_URL}/session/${sessionId}`,
            { method: 'DELETE' }
        );

        if (deleteResponse.ok) {
            console.log('✅ Ephemeral RAG 세션 삭제 완료');
        }
    } catch (error) {
        console.warn('⚠️ 세션 삭제 오류 (무시):', error);
    }

    alert('✅ 모든 아이디어가 저장되었습니다!');
    location.href = 'index.html';
}

Spring Boot API

@PostMapping
public ResponseEntity<IdeaResponseDto> createIdea(
    @RequestBody IdeaRequestDto requestDto
) {
    Idea idea = requestDto.toEntity();
    Idea savedIdea = ideaService.save(idea);
    IdeaResponseDto responseDto = IdeaResponseDto.from(savedIdea);
    return ResponseEntity.ok(responseDto);
}

MySQL에 저장된 데이터

SELECT * FROM ideas;

+----------+---------+------------------------------------------+
| idea_id  | user_id | title                                    |
+----------+---------+------------------------------------------+
| 1        | 2       | 10분 브이로그 - 일상의 작은 순간들        |
| 2        | 2       | 먹방 챌린지 시리즈                        |
| 3        | 2       | 제품 리뷰 + 사용 팁                       |
+----------+---------+------------------------------------------+

-- content 필드 (JSON)
{
  "description": "💡 핵심 문제:\n유튜버들이 긴 영상...",
  "analysis": "📊 분석 결과:\n• 강점: ...",
  "generatedAt": "2024-12-03T10:30:00Z"
}

👁️ 6. 저장된 아이디어 다시보기

사이드바 클릭 이벤트

function displayIdeas(ideas) {
    ideas.forEach(idea => {
        const ideaItem = document.createElement('div');
        ideaItem.className = 'idea-item';
        ideaItem.style.cursor = 'pointer';
        ideaItem.innerHTML = `
            <div class="idea-date">${formatDate(idea.createdAt)}</div>
            <div class="idea-title">${idea.title}</div>
            <button onclick="deleteIdea(${idea.ideaId}, event)">🗑️</button>
        `;

        // ✅ 클릭 이벤트 추가
        ideaItem.addEventListener('click', () => {
            showIdeaDetail(idea.ideaId);
        });

        sidebarContent.appendChild(ideaItem);
    });
}

상세보기 모달

async function showIdeaDetail(ideaId) {
    const token = localStorage.getItem('token');

    // API 호출
    const response = await fetch(
        `http://localhost:8080/api/ideas/${ideaId}`,
        { headers: { 'Authorization': `Bearer ${token}` }}
    );

    const idea = await response.json();

    // content는 JSON 문자열 → 파싱
    let ideaContent;
    try {
        ideaContent = JSON.parse(idea.content);
    } catch (e) {
        ideaContent = {
            description: idea.content,
            analysis: ''
        };
    }

    // 모달 생성
    const modal = document.createElement('div');
    modal.id = 'ideaDetailModal';
    modal.innerHTML = `
        <div style="background: white; padding: 2.5rem; ...">
            <button onclick="closeIdeaModal()">×</button>

            <h2>💡 ${idea.title}</h2>
            <div>생성일: ${formatDate(idea.createdAt)}</div>

            <div style="background: #f8f9fa; padding: 2rem;">
                <h3>📝 아이디어 설명</h3>
                <div>${ideaContent.description}</div>
            </div>

            ${ideaContent.analysis ? `
                <div style="background: #e8f5e9; padding: 2rem;">
                    <h3>📊 분석 결과</h3>
                    <div>${ideaContent.analysis}</div>
                </div>
            ` : ''}

            <button onclick="closeIdeaModal()">닫기</button>
        </div>
    `;

    document.body.appendChild(modal);
}

모달 외부 클릭 시 닫기:

modal.addEventListener('click', (e) => {
    if (e.target === modal) {
        closeIdeaModal();
    }
});

🗑️ 7. Ephemeral RAG 자동 삭제

문제: 세션 데이터가 쌓임

python-service/domain/brainstorming/data/ephemeral/
├── session_abc123/
├── session_def456/
├── session_ghi789/
└── ... (계속 쌓임)

해결 1: 저장 후 즉시 삭제

// saveIdeas() 함수 내부
try {
    const deleteResponse = await fetch(
        `${API_BASE_URL}/session/${sessionId}`,
        { method: 'DELETE' }
    );

    if (deleteResponse.ok) {
        console.log('✅ Ephemeral RAG 세션 삭제 완료');
    }
} catch (error) {
    console.warn('⚠️ 세션 삭제 오류 (무시)');
}

해결 2: 서버 시작 시 오래된 세션 청소

@router.post("/session")
async def create_session():
    # 🧹 5분 이상 된 세션 폴더 자동 정리
    ephemeral_base_dir = Path(...) / "data" / "ephemeral"

    if ephemeral_base_dir.exists():
        deleted_count = 0
        current_time = time.time()
        cutoff_time = current_time - 300  # 5분

        for session_dir in ephemeral_base_dir.iterdir():
            mtime = session_dir.stat().st_mtime

            if mtime < cutoff_time and len(list(session_dir.iterdir())) == 0:
                shutil.rmtree(session_dir)
                deleted_count += 1
                logger.info(f"   - 삭제: {session_dir.name}")

        logger.info(f"✅ {deleted_count}개의 오래된 폴더 삭제됨")

Python 로그:

🧹 오래된 Ephemeral 세션 폴더 청소 시작...
   - 삭제: session_abc123... (생성 후 6.3분 경과)
   - 삭제: session_def456... (생성 후 7.1분 경과)
✅ 2개의 오래된 빈 폴더 삭제됨

💬 8. 문의하기 시스템

사용자 페이지 (inquiry.html)

탭 구조:

<div class="tab-menu">
    <button class="tab-button active" onclick="switchTab('list')">
        📋 내 문의 목록
    </button>
    <button class="tab-button" onclick="switchTab('write')">
        ✍️ 문의 작성
    </button>
</div>

<div id="listTab" class="tab-content active">
    <!-- 문의 목록 -->
</div>

<div id="writeTab" class="tab-content">
    <!-- 작성 폼 -->
</div>

문의 목록 카드:

function displayInquiries(inquiries) {
    inquiries.forEach(inquiry => {
        const card = document.createElement('div');
        card.className = 'inquiry-card';
        card.innerHTML = `
            <div class="inquiry-header">
                <h3>${inquiry.title}</h3>
                <span class="status-badge ${inquiry.status.toLowerCase()}">
                    ${getStatusText(inquiry.status)}
                </span>
            </div>
            <div class="inquiry-date">
                ${formatDate(inquiry.createdAt)}
            </div>
        `;

        card.addEventListener('click', () => {
            showDetailModal(inquiry.inquiryId);
        });

        listContainer.appendChild(card);
    });
}

function getStatusText(status) {
    switch(status) {
        case 'PENDING': return '답변 대기';
        case 'ANSWERED': return '답변 완료';
        case 'CLOSED': return '종료';
    }
}

상세 모달:

async function showDetailModal(inquiryId) {
    const response = await fetch(
        `http://localhost:8080/api/inquiries/${inquiryId}`,
        { headers: { 'Authorization': `Bearer ${token}` }}
    );

    const inquiry = await response.json();

    const modal = `
        <div class="modal-content">
            <h2>${inquiry.title}</h2>
            <span class="status">${getStatusText(inquiry.status)}</span>

            <div class="inquiry-content">
                <h3>📝 문의 내용</h3>
                <p>${inquiry.content}</p>
            </div>

            ${inquiry.reply ? `
                <div class="reply-section">
                    <h3>💬 관리자 답변</h3>
                    <p>${inquiry.reply}</p>
                </div>
            ` : '<p class="no-reply">아직 답변이 없습니다.</p>'}

            ${inquiry.status === 'PENDING' ? `
                <button onclick="editInquiry(${inquiryId})">✏️ 수정</button>
                <button onclick="deleteInquiry(${inquiryId})">🗑️ 삭제</button>
            ` : ''}
        </div>
    `;
}

관리자 페이지 (admin.html)

접근 권한 확인:

async function checkAdminAccess() {
    const token = localStorage.getItem('token');

    const response = await fetch('http://localhost:8080/api/auth/me', {
        headers: { 'Authorization': `Bearer ${token}` }
    });

    const user = await response.json();

    if (user.role !== 'ADMIN') {
        alert('❌ 관리자 권한이 필요합니다.');
        location.href = 'index.html';
        return;
    }

    // ✅ 관리자 확인됨
    loadAllInquiries();
}

답변 작성:

async function submitReply(inquiryId) {
    const reply = document.getElementById('replyInput').value;

    const response = await fetch(
        `http://localhost:8080/api/inquiries/${inquiryId}/reply`,
        {
            method: 'PUT',
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ reply })
        }
    );

    if (response.ok) {
        alert('✅ 답변이 등록되었습니다.');
        loadAllInquiries();
    }
}

😵 9. 삽질 총정리

삽질 1: 경로 혼동 (가장 큰 삽질)

문제:

# 내가 작업한 경로
/home/claude/brainstorming-platform/

# 실제 프로젝트 경로
/Users/jinmokim/Projects/PersonalProject/brainstorming/

파일 수정했는데 브라우저에 반영 안 됨...

원인: 엉뚱한 곳에서 작업함 😱

해결: IntelliJ 도구 사용

// ✅ 올바른 방법
jetbrains:create_new_file_with_text
pathInProject: "frontend/inquiry.html"

// ❌ 절대 금지
bash: cat > /home/claude/file.html

교훈: 작업 전 항상 경로 확인! 🚨

삽질 2: JWT 토큰 role 누락

증상: 관리자인데 관리자 페이지 접속 안 됨

원인: JWT에 role 정보 없음

해결:

  1. JwtTokenProvider에 role 추가
  2. localStorage.clear() → 재로그인

삽질 3: Idea Entity에 status 필드 없음

증상:

// JavaScript에서 보냄
{
  userId: 1,
  title: "아이디어",
  content: "...",
  purpose: "...",
  status: "PUBLISHED"  // ❌
}

DB:

@Entity
public class Idea {
    private Long ideaId;
    private Long userId;
    private String title;
    private String content;
    private String purpose;
    // ❌ status 필드 없음!
}

오류: 400 Bad Request

해결: JavaScript에서 status 제거

삽질 4: 브라우저 캐시

문제: 코드 수정했는데 변경 안 됨

해결: Ctrl + F5 (강력 새로고침)

또는:

// 캐시 무효화 쿼리 추가
<script src="js/main.js?v=1.0.1"></script>

💡 10. 배운 점

시스템 통합의 중요성

  • 백엔드만 잘 만들어도 안 됨
  • 프론트엔드만 예뻐도 안 됨
  • 전체가 연결되어야 진짜 서비스!

데이터 흐름 이해

사용자 입력
  ↓
프론트엔드 (JavaScript)
  ↓
Python FastAPI (AI 처리)
  ↓
프론트엔드 (결과 표시)
  ↓
Spring Boot (저장)
  ↓
MySQL (영구 보존)

전체 흐름을 이해해야 디버깅 가능!

세션 관리의 중요성

  • Ephemeral RAG는 임시 데이터
  • 저장 후 반드시 삭제
  • 오래된 세션 자동 청소 필요

사용자 경험 (UX)

  • 로딩 중 메시지 표시
  • 에러 발생 시 안내
  • 버튼 활성화/비활성화
  • 진행 상태 표시

작은 디테일이 큰 차이를 만듦!


🚀 11. 다음 단계

디자인 개선

  • CSS 다듬기
  • 반응형 디자인
  • 애니메이션 추가

기능 추가

  • 아이디어 수정 기능
  • 아이디어 태그 시스템
  • 즐겨찾기 기능
  • 공유 기능

배포 준비 (진짜 중요! 🔥)

  • Docker Compose 설정
  • GitHub Actions CI/CD
  • AWS 배포

📊 12. 최종 시스템 구조

┌─────────────────────────────────────────────────────────┐
│                 프론트엔드 (Vanilla JS)                   │
│  • index.html (메인)                                     │
│  • brainstorm.html (AI 브레인스토밍)                     │
│  • inquiry.html (문의하기)                               │
│  • admin.html (관리자)                                   │
└──────────────┬──────────────────┬───────────────────────┘
               │                  │
      OAuth 2.0 로그인      브레인스토밍 요청
      JWT 토큰 발급         아이디어 생성
               │                  │
               ▼                  ▼
┌──────────────────────┐  ┌──────────────────────┐
│  Spring Boot 8080    │  │  Python FastAPI 8000 │
│  • OAuth2 + JWT      │  │  • OpenAI GPT-4      │
│  • User CRUD         │  │  • Ephemeral RAG     │
│  • Idea CRUD         │  │  • 브레인스토밍 엔진   │
│  • Inquiry CRUD      │  │  • SWOT 분석         │
│  • 관리자 권한 체크   │  │  • 세션 자동 정리     │
└──────┬───────────────┘  └──────────────────────┘
       │
       ▼
┌──────────────────────┐
│   MySQL 3306         │
│  • users (5개)       │
│  • ideas (12개)      │
│  • inquiries (3개)   │
└──────────────────────┘

테이블 통계:

  • Users: 5명 (1명 관리자, 4명 일반)
  • Ideas: 12개 (브레인스토밍으로 생성)
  • Inquiries: 3개 (2개 답변 완료, 1개 대기)

💬 13. 마무리

와... 진짜 길었다 😭

H2 → MySQL 전환하고,
JWT에 role 추가하고,
브레인스토밍 전체 플로우 만들고,
아이디어 저장하고,
상세보기 모달 만들고,
Ephemeral RAG 자동 삭제하고...

특히 힘들었던 것:

  1. 경로 혼동 (진짜 시간 많이 날림)
  2. JWT role 추가 후 재로그인 필요했던 것
  3. Ephemeral RAG 세션 관리

뿌듯한 것:

  1. 전체 시스템이 드디어 연결됨!
  2. 실제로 쓸 수 있는 서비스가 됨!
  3. 아이디어 생성 → 저장 → 다시보기 완벽!

이제 진짜 배포만 하면 된다!

다음 목표: Docker Compose 설정 → CI/CD → AWS 배포


작성일: 2024-12-03
개발 시간: 약 10시간
GitHub: [저장소 링크]

#SpringBoot #Python #FastAPI #MySQL #JWT #OAuth2 #AI #브레인스토밍 #LLM #RAG


'[그래서 일단 프로젝트] > [개인프로젝트-IdeaMaker]' 카테고리의 다른 글

기록 7. OAuth 2.0 / JWT 구현 (0)
기록 6. 컨트롤러 계층 완성 및 테스트완료 (0)
기록 5. DTO 작성 및 Test 코드 작성 단계 (0)

<type>(<scope>): <짧은 설명>
예) feat(auth): 카카오/네이버 OAuth 스코프 수정

feat 새로운 기능 추가할 때 (화면 추가, API 추가 등)
fix 버그 수정 (NullPointer, 잘못된 응답, 잘못된 로직)
hotfix 긴급하게 바로 배포해야 하는 치명적 버그 수정
refactor 기능 변화 없이 코드 구조/이름/로직 개선할 때 (네가 말한 modifier류는 이쪽에 가깝다)
style 코드 포맷팅, 들여쓰기, 세미콜론, 개행 등 (로직 변화 X)
docs README, API 문서, 주석만 수정할 때
test 테스트 코드 추가/수정
chore 빌드 스크립트, 패키지 버전, 설정 파일 등 잡일성 변경
build 빌드 시스템/의존성 관련 변경 (Gradle, npm deps 등)
ci GitHub Actions, Jenkins 등 CI 설정 수정
perf 성능 개선 (쿼리 최적화, 캐시 추가 등)
revert 이전 커밋 되돌릴 때

 

 

우리 프로젝트는 실제적으로 1개의 채팅창 화면을 챗봇이라하고...

이 챗봇에게 회사상품, 회사HR기준,아이디어제작기, 심리상담, 보고서자동화 기능을 처리하기위해서 만들었다.

 

1개의 챗봇이 여러기능을 하기위해서 각 모듈별로 작업을 했던거고

 

이제 각자  모듈이 완성되어가고 있는 시점에 , 챗봇의 역할이 매우 중요해졌음..

구조적으로 에이전트 완성은 되었으나... 나중에 기억하기위해서라도 정리를 좀 해두... 해야겠다.

 

 

 

 

 

캐릭터는 숨길 수 있도록 토글기능 넣어뒀고, 채팅창도 크게 키우고....

 

아래와 같은 챗봇 1개에서 여러개의 모듈을 LLM이 알아서 호출할 수 있도록 구현해두었다.(아직 미구현된 기능들은 플로차트에 빠져있음)

 

 

 

먼저 챗봇에서  멀티에이전트를 불러온다.

 

export async function sendMultiAgentMessage(userMessage) {
  try {
    console.log('🤖 Multi-Agent 메시지 전송:', userMessage);

    // 세션 ID 가져오기 (실패해도 계속 진행)
    let sessionId = null;
    try {
      sessionId = await getOrCreateMultiAgentSession();
    } catch (error) {
      console.warn('⚠️ 세션 생성 실패, 세션 없이 진행:', error);
      // 세션 없이도 진행 가능 (백엔드가 선택적으로 처리)
    }

    const headers = {
      'Content-Type': 'application/json',
    };

    if (accessToken) {
      headers['Authorization'] = `Bearer ${accessToken}`;
    }

    const requestBody = {
      query: userMessage
    };

    if (sessionId) {
      requestBody.session_id = sessionId;
    }

    const response = await fetch(`${API_BASE_URL}/multi-agent/query`, {
      method: 'POST',
      headers: headers,
      credentials: 'include',
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      const errorText = await response.text();
      console.error(`❌ Multi-Agent API 호출 실패: ${response.status} ${response.statusText}`, errorText);
      throw new Error(`Multi-Agent API 호출 실패: ${response.status} ${response.statusText}`);
    }

    const result = await response.json();
    console.log('🤖 Multi-Agent 응답:', result);

    return result;

  } catch (error) {
    console.error('❌ Multi-Agent API 오류:', error);
    throw error;
  }
}

 

 

const response = await fetch(`${API_BASE_URL}/multi-agent/query`, {
method: 'POST',
headers: headers,
credentials: 'include',
body: JSON.stringify(requestBody)
});

 

요 문구 부분이 백엔드 API를 호출하게된다. 

 

그럼 엔드포인트를 관리하는 multi_agent.py 파일은..

내부 로직에 따라 각모듈을 찾아주는 역할을하는데 그게..아래 부분이다.

 

# Supervisor agent 싱글톤 가져오는 함수
def get_supervisor_agent() -> SupervisorAgent:
    
    global _supervisor_agent
    if _supervisor_agent is None:
        _supervisor_agent = SupervisorAgent()
    return _supervisor_agent

# 사용자 질문을 적절한 에이전트에게 전달하는 함수
@router.post("/query", response_model=MultiAgentResponse)
async def multi_agent_query(
    request: MultiAgentRequest,
    supervisor: SupervisorAgent = Depends(get_supervisor_agent)
):

    try:
        response = await supervisor.process(request)
        return response
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Multi-Agent 처리 중 오류 발생: {str(e)}"
        )

# 사용 가능한 에이전트 목록 조회
@router.get("/agents", response_model=List[Dict[str, Any]])
async def get_available_agents(
    supervisor: SupervisorAgent = Depends(get_supervisor_agent)
):

    try:
        agents = supervisor.get_available_agents()
        return agents
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"에이전트 목록 조회 중 오류 발생: {str(e)}"
        )

 

 

 

그럼 슈퍼바이저는.. 아래내용인 프롬프트 엔지니어링을 통해서 필요한 모듈을 찾아가게 한다. 

 

**당신의 역할:**
1. 사용자의 질문을 이해하고 의도를 파악합니다.
2. 질문에 포함된 키워드와 맥락을 분석합니다.
3. 질문에 가장 적합한 전문 에이전트를 선택합니다.
4. 선택한 에이전트에게 작업을 위임하고 결과를 받습니다.
5. 최종 결과를 사용자에게 명확하고 친절하게 전달합니다.

**사용 가능한 전문 에이전트:**
1,2 생략!!!

3. **brainstorming_tool**: 창의적 아이디어 발상 및 브레인스토밍 지원
   - 사용 조건:
     * 사용자가 구체적인 아이디어나 해결책을 필요로 하는 상황
     * 단순한 정보 질문이 아닌, 실제로 아이디어 생성을 원하는 경우
     * "브레인스토밍이 뭐야?" 같은 개념 설명 요청은 chatbot_tool 사용
   - 트리거 상황:
     * 명시적 요청: "브레인스토밍 해줘", "아이디어 만들어줘", "기획 도와줘"
     * 막힌 상황: "좋은 생각이 안 떠올라", "기획이 막혔어", "아이디어가 필요해"
     * 창의적 요구: "새로운 방법이 필요해", "참신한 아이디어 좀", "혁신적인 접근법"
   - 예시 (도구 사용 O):
     * "새로운 마케팅 아이디어를 내고 싶어"
     * "프로젝트 기획이 막혔는데 도와줘"
     * "좋은 생각이 안 떠올라"
     * "브레인스토밍 해줘"
     * "창의적인 해결책이 필요해"
   - 예시 (도구 사용 X - chatbot_tool 사용):
     * "브레인스토밍이 뭐야?" → 개념 설명 요청
     * "아이디어를 만든다는 게 뭐야?" → 정보 질문
     * "브레인스토밍 방법 알려줘" → 기법 설명 요청


4,5,6~ 생략 !!! 

**에이전트 선택 가이드:**
1. **"브레인스토밍"이라는 단어가 명시적으로 포함되면 무조건 brainstorming_tool을 선택하세요.**
2. 질문에 포함된 키워드를 먼저 확인하세요.
3. 여러 에이전트의 키워드가 겹치면, 질문의 주요 목적을 파악하세요.
4. 감정적 표현(힘들어, 우울해 등)이 있으면 therapy_tool을 우선 고려하세요.
5. 회사/업무 관련 정보 요청은 rag_tool을 사용하세요.
6. 일반적인 인사나 잡담은 chatbot_tool을 사용하세요.

**중요한 규칙:**
- 질문의 핵심 의도를 정확히 파악하세요.
- 가장 적합한 에이전트 하나를 선택하세요.
- **brainstorming_tool을 선택한 경우, 절대 직접 답변을 생성하지 말고 에이전트의 안내 메시지만 그대로 전달하세요.**
- **적절한 에이전트(도구)를 사용하는 경우, 챗봇이 직접 길게 설명하지 말고 간결하게 해당 모듈 사용을 안내하세요.**
- 에이전트의 응답을 그대로 사용자에게 전달하세요.
- 한국어로 응답하세요.
"""

 

 

이 문장을 지피티(LLM)이 챗봇으로써 파악하고 실행할 모듈을 찾아가게한다.

 

ai 답변을 빌어 ..

  1. Python 코드 (
     
    BrainstormingAgent):
    • 사용자님이 작성하신 이 클래스는 **실제 작업자(Worker)**입니다.
    • 하지만 LLM(Supervisor)은 이 파이썬 코드를 직접 읽거나 실행할 수 없습니다.
  2. 도구 등록 (
    ):
    •  
      supervisor.py에서 를 호출할 때, 이 에이전트가 **"도구(Tool)"**라는 형태로 포장되어 LLM에게 전달됩니다.
    • 이때 LLM에게는 코드가 아니라 **JSON 스키마(설명서)**만 전달됩니다.
    • 예시: "이 도구의 이름은 이고, 
       
      query라는 문자열 인자를 받으며, 창의적인 아이디어가 필요할 때 사용합니다."
  3. LLM의 판단 (Function Calling):
    • Supervisor(LLM)가 사용자 질문("아이디어 만들어줘")을 받으면, 프롬프트에 적힌 규칙을 보고 생각합니다.
    • "아, 이건 을 써야겠군!"
    • 그리고 LLM은 텍스트 답변 대신 **특수한 신호(JSON)**를 뱉어냅니다.
    • 출력 예시: 
  4. 실제 실행 (Runtime):
    • LangGraph 프레임워크가 이 신호를 감지하고, 실제로 Python의 
       
      process() 함수를 실행합니다.
    • 그리고 그 결과()를 다시 LLM에게 돌려줍니다.

 

 

요렇게 해서 최종적으로 

"""
Brainstorming Agent

브레인스토밍 및 창의적 아이디어 제안 에이전트
기존 BrainstormingService를 활용합니다.
"""

from typing import Dict, Any, Optional
from .base_agent import BaseAgent

# 브레인스토밍 에이전트 클래스
class BrainstormingAgent(BaseAgent):

    # 초기화 함수    
    def __init__(self):
        super().__init__(
            name="brainstorming",
            description="창의적인 아이디어 발상과 브레인스토밍 기법을 제안하는 에이전트입니다. "
                       "문제 해결, 아이디어 도출, 창의적 사고 방법 등을 안내합니다."
        )
        # Lazy loading: 실제 사용 시에만 BrainstormingService 로드
        self._brainstorming_service = None
    
    # @property: 메소드를 변수처럼 사용할 수 있게 해주는 기능
    @property
    def brainstorming_service(self):
        """BrainstormingService lazy loading"""
        if self._brainstorming_service is None:
            from app.domain.brainstorming.service import BrainstormingService
            self._brainstorming_service = BrainstormingService()
        return self._brainstorming_service
    
    # 브레인스토밍 진행하는 비동기 함수
    async def process(self, query: str, context: Optional[Dict[str, Any]] = None) -> str:

        try:
            # Supervisor가 이 에이전트를 선택했다는 것은
            # 사용자가 아이디어/브레인스토밍이 필요한 상황이라는 의미
            # RAG 검색 없이 바로 브레인스토밍 도구 사용을 제안
            
            print(f"[BrainstormingAgent] 쿼리: {query}")
            print(f"[BrainstormingAgent] 브레인스토밍 도구 제안 모드")
            
            # 사용자의 쿼리에서 주제 추출 (간단하게)
            # 예: "빵집 매출 증대 아이디어" -> "빵집 매출 증대"
            topic_hint = ""
            if "빵집" in query or "카페" in query or "가게" in query:
                topic_hint = "관련 "
            elif "마케팅" in query:
                topic_hint = "마케팅 "
            elif "프로젝트" in query or "기획" in query:
                topic_hint = "프로젝트 "
            
            # 간결한 제안 메시지 반환
            return f"SUGGESTION: 브레인스토밍 도구로 {topic_hint}아이디어를 함께 만들어볼까요? 🚀"
            
        except Exception as e:
            print(f"[BrainstormingAgent] 오류: {e}")
            return f"브레인스토밍 제안 중 오류가 발생했습니다: {str(e)}"
    
    # 브레인스토밍 에이전트 기능 목록 리턴
    def get_capabilities(self) -> list:
        
        return [
            "창의적 아이디어 제안",
            "브레인스토밍 기법 안내",
            "문제 해결 방법 제시",
            "협업 방법 제안",
            "혁신적 사고 촉진",
        ]

요녀석이 .. 메인모듈을 찾아간다. 

 

 

복잡하구나....

초보 개발자 지망생이 이걸 ai 없이 짤수있나 ?... ㅋㅋㅋ 

개념만 머리에...

+ Recent posts