🧠 전체 시스템 통합 완성 - 브레인스토밍부터 저장까지
🎯 오늘의 목표
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_def4565분 이상 된 세션은 자동으로 청소됨! 깔끔! 🧹
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 정보 없음
해결:
- JwtTokenProvider에 role 추가
- 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 자동 삭제하고...
특히 힘들었던 것:
- 경로 혼동 (진짜 시간 많이 날림)
- JWT role 추가 후 재로그인 필요했던 것
- Ephemeral RAG 세션 관리
뿌듯한 것:
- 전체 시스템이 드디어 연결됨!
- 실제로 쓸 수 있는 서비스가 됨!
- 아이디어 생성 → 저장 → 다시보기 완벽!
이제 진짜 배포만 하면 된다!
다음 목표: 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) |
'그래서 일단 프로젝트 > 개인프로젝트-IdeaMaker(클로드 작성본 md 모음)' 카테고리의 다른 글
| 기록 9 . 최종 배포까지... (도커, AWS) 이제... 잠시 쉬었다가 추후 사후관리를.. (1) | 2025.12.04 |
|---|---|
| 기록 7 . OAuth2.0 / JWT 구현...너무 어려운데 ?? (0) | 2025.12.02 |
| 기록 6 . 파이썬 모듈 JAVA 백엔드에 연결하기 .. (1) | 2025.12.01 |
| 기록 5 . 컨트롤러 계층 완성 및 테스트완료 (0) | 2025.11.27 |
| 기록 4 . DTO 작성 및 Test 코드 작성 단계 (0) | 2025.11.26 |