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

🎯 오늘의 목표

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)

+ Recent posts