def is_streamlit_cloud():
"""Streamlit Cloud 환경 확인 (Check Streamlit Cloud)"""
try:
from streamlit.runtime import exists
return exists()
except ImportError:
return False
Step 3: 통합 환경 감지 (Unified Detection)
def is_premium_unlocked():
"""프리미엄 기능 잠금 해제 여부 (Check if premium is unlocked)"""
# 1. 로컬 개발 모드 (Local dev mode)
if is_local_dev():
return True
# 2. 세션에 이미 인증됨 (Already authenticated)
if st.session_state.get('premium_unlocked', False):
return True
# 3. 잠금 상태 (Locked)
return False
🔧 환경 변수 설정 (Environment Variable Setup)
로컬 개발 (Local Development)
방법 1: 터미널에서 일회성 (Temporary in Terminal)
# 실행 전
export LOTTO_DEV_MODE="true"
# Streamlit 앱 실행
streamlit run src/web_app.py
# 확인
echo $LOTTO_DEV_MODE
방법 2: 쉘 설정 파일 (Shell Configuration)
# ~/.zshrc 또는 ~/.bashrc 편집
export LOTTO_DEV_MODE="true"
# 재로드
source ~/.zshrc
방법 3: .env 파일 (Environment File)
# .env 파일 생성
echo "LOTTO_DEV_MODE=true" > .env
# python-dotenv 사용
pip install python-dotenv
# web_app.py 상단
from dotenv import load_dotenv
load_dotenv()
Streamlit Cloud (Production)
설정 위치:
Streamlit Cloud 대시보드 접속
App Settings > Secrets
환경 변수 설정 안 함 (의도적으로 비활성화)
액세스 코드 필요
⚠️ 주의사항 (Important Notes)
로컬에서 프리미엄 기능 테스트 후 배포 전:
# 환경 변수 제거 (Unset variable)
unset LOTTO_DEV_MODE
# 확인 (Verify)
echo $LOTTO_DEV_MODE # 출력 없어야 함
import re
def validate_code_format(code):
"""코드 형식 검증 (Validate code format)"""
# 패턴: PREM-XXXX-XXXX (Pattern)
pattern = r'^PREM-[A-Z0-9]{4}-[A-Z0-9]{4}$'
return re.match(pattern, code) is not None
# 테스트 (Test)
print(validate_code_format("PREM-A1B2-C3D4")) # True
print(validate_code_format("prem-a1b2-c3d4")) # False (소문자)
print(validate_code_format("PREM-AB-CDEF")) # False (길이)
전체 검증 프로세스 (Full Validation Process)
def verify_access_code(input_code, valid_codes):
"""액세스 코드 검증 (Verify access code)
Args:
input_code: 사용자 입력 (User input)
valid_codes: 유효한 코드 목록 (Valid codes list)
Returns:
bool: 검증 성공 여부 (Validation result)
"""
# 1. 정규화 (Normalization)
normalized = input_code.upper().strip()
# 2. 형식 검증 (Format validation)
if not validate_code_format(normalized):
return False, "Invalid format"
# 3. 코드 존재 확인 (Check existence)
if normalized not in valid_codes:
return False, "Code not found"
# 4. 성공 (Success)
return True, "Valid code"
📊 코드 통계 (Code Statistics)
총 가능한 조합 수 (Total Possible Combinations):
36^8 = 2,821,109,907,456
약 2.8조 개 (About 2.8 trillion)
100개 코드의 충돌 확률 (Collision Probability for 100 codes):
P(collision) ≈ 0.00000177% (거의 0, Nearly zero)
보안 강도 (Security Strength):
무차별 대입 (Brute force): 2.8조 시도 필요
100개 중 하나 맞추기: 1/100 = 1%
형식까지 맞추기: 거의 불가능
4. Streamlit Secrets 관리 (Streamlit Secrets Management)
import streamlit as st
def load_premium_codes():
"""프리미엄 액세스 코드 로드 (Load premium access codes)"""
try:
if "premium" in st.secrets:
return st.secrets["premium"]["access_codes"]
else:
st.error("❌ Secrets not configured!")
return []
except Exception as e:
st.error(f"❌ Error loading secrets: {e}")
return []
에러 처리 (Error Handling)
def check_access_code(input_code):
"""액세스 코드 검증 (Check access code)"""
# Secrets 로드 (Load secrets)
valid_codes = load_premium_codes()
if not valid_codes:
return False, "Secrets not available"
# 정규화 (Normalize)
normalized = input_code.upper().strip()
# 검증 (Validate)
if normalized in valid_codes:
return True, "Access granted"
else:
return False, "Invalid code"
🛡️ 보안 모범 사례 (Security Best Practices)
절대 금지 (Never Do)
❌ 코드에 직접 하드코딩 (Hardcode in code)
# 절대 안 됨! (Never do this!)
valid_codes = ["PREM-A1B2-C3D4", "PREM-QWER-TYUI"]
❌ Git에 커밋 (Commit to Git)
# 절대 안 됨! (Never do this!)
git add .streamlit/secrets.toml
git commit -m "Add secrets"
❌ 공개 저장소에 노출 (Expose in public repo)
# README.md에 코드 공개 금지 (Don't share in README)
권장 사항 (Recommended)
✅ Streamlit Secrets 사용 (Use Streamlit Secrets)
# 올바른 방법 (Correct way)
codes = st.secrets["premium"]["access_codes"]
✅ .gitignore 추가 (Add to .gitignore)
.streamlit/secrets.toml
.env
✅ 로컬과 클라우드 동기화 (Sync local and cloud)
로컬 파일 수정 후 클라우드 Secrets도 업데이트
✅ 접근 로그 기록 (Log access attempts)
import datetime
def log_access_attempt(code, success):
"""액세스 시도 로그 (Log access attempt)"""
timestamp = datetime.datetime.now()
status = "SUCCESS" if success else "FAILURE"
print(f"[{timestamp}] {status}: {code[:8]}****")
import streamlit as st
def init_session_state():
"""세션 상태 초기화 (Initialize session state)"""
# 프리미엄 잠금 상태 (Premium lock state)
if 'premium_unlocked' not in st.session_state:
st.session_state.premium_unlocked = False
# 액세스 코드 입력 횟수 (Access attempts)
if 'access_attempts' not in st.session_state:
st.session_state.access_attempts = 0
# 마지막 액세스 시간 (Last access time)
if 'last_access_time' not in st.session_state:
st.session_state.last_access_time = None
# 앱 최상단에서 호출 (Call at top of app)
init_session_state()
🔓 잠금 해제 UI (Unlock UI)
프리미엄 기능 잠금 화면
def show_premium_unlock_ui():
"""프리미엄 잠금 해제 UI (Premium unlock UI)"""
st.title("🔒 Premium Features Locked")
st.markdown("""
**프리미엄 기능 (Premium Features)**에는 액세스 코드가 필요합니다.
**포함 기능:**
- 백테스팅 시스템 (Backtesting System)
- 가중치 최적화 (Weight Optimization)
- 성능 메트릭 (Performance Metrics)
- 실시간 재학습 (Real-time Retraining)
""")
# 액세스 코드 입력 (Access code input)
col1, col2 = st.columns([3, 1])
with col1:
code_input = st.text_input(
"액세스 코드 (Access Code)",
placeholder="PREM-XXXX-XXXX",
max_chars=14,
key="premium_code_input"
)
with col2:
st.markdown("<br>", unsafe_allow_html=True) # 정렬용
unlock_button = st.button("🔓 Unlock", type="primary")
# 잠금 해제 시도 (Unlock attempt)
if unlock_button:
if verify_premium_code(code_input):
st.session_state.premium_unlocked = True
st.success("✅ Premium features unlocked!")
st.balloons()
st.rerun()
else:
st.session_state.access_attempts += 1
st.error("❌ Invalid access code. Please try again.")
# 시도 횟수 표시 (Show attempts)
if st.session_state.access_attempts >= 3:
st.warning(f"⚠️ Failed attempts: {st.session_state.access_attempts}")
✅ 코드 검증 함수 (Code Verification Function)
import datetime
def verify_premium_code(input_code):
"""프리미엄 코드 검증 (Verify premium code)
Args:
input_code: 사용자 입력 코드 (User input code)
Returns:
bool: 검증 성공 여부 (Validation result)
"""
# 1. Secrets 로드 (Load secrets)
try:
valid_codes = st.secrets["premium"]["access_codes"]
except Exception as e:
st.error(f"❌ Error loading secrets: {e}")
return False
# 2. 입력 정규화 (Normalize input)
normalized = input_code.upper().strip()
# 3. 형식 검증 (Format validation)
import re
pattern = r'^PREM-[A-Z0-9]{4}-[A-Z0-9]{4}$'
if not re.match(pattern, normalized):
return False
# 4. 코드 존재 확인 (Check existence)
if normalized in valid_codes:
# 성공 로그 (Success log)
st.session_state.last_access_time = datetime.datetime.now()
return True
else:
return False
🎨 조건부 UI 렌더링 (Conditional UI Rendering)
프리미엄 상태에 따른 UI 분기
def backtesting_page():
"""백테스팅 페이지 (Backtesting page)"""
st.title("🔬 Backtesting Results")
# 프리미엄 확인 (Check premium)
if not is_premium_unlocked():
# 잠금 화면 표시 (Show lock screen)
show_premium_unlock_ui()
st.stop() # 여기서 중단 (Stop here)
# --- 프리미엄 사용자만 아래 코드 실행 ---
# (Only premium users reach here)
st.success("✅ Premium features unlocked!")
# 백테스팅 UI (Backtesting UI)
tab1, tab2, tab3 = st.tabs([
"📊 Backtesting Results",
"⚙️ Weight Optimization",
"🚀 Real-time Retraining"
])
with tab1:
# 백테스팅 결과 표시
show_backtesting_results()
with tab2:
# 가중치 최적화 UI
show_weight_optimization()
with tab3:
# 실시간 재학습 UI
show_realtime_retraining()
🔄 세션 재시작 (Session Rerun)
상태 변경 후 UI 갱신
# 잠금 해제 후 페이지 새로고침 (Rerun after unlock)
if st.button("🔓 Unlock"):
if verify_premium_code(code_input):
st.session_state.premium_unlocked = True
st.rerun() # 페이지 즉시 새로고침 (Immediate refresh)
🗑️ 로그아웃 기능 (Logout Feature)
세션 상태 초기화
def logout():
"""로그아웃 (Logout)"""
# 프리미엄 상태 해제 (Clear premium state)
st.session_state.premium_unlocked = False
st.session_state.access_attempts = 0
st.session_state.last_access_time = None
st.success("✅ Logged out successfully!")
st.rerun()
# 사이드바에 로그아웃 버튼 (Logout button in sidebar)
if st.session_state.premium_unlocked:
if st.sidebar.button("🚪 Logout"):
logout()
📊 세션 정보 표시 (Session Info Display)
def show_session_info():
"""세션 정보 표시 (Show session info)"""
with st.expander("🔍 Session Information"):
st.write(f"**Premium Status:** {'Unlocked ✅' if st.session_state.premium_unlocked else 'Locked 🔒'}")
st.write(f"**Access Attempts:** {st.session_state.access_attempts}")
if st.session_state.last_access_time:
st.write(f"**Last Access:** {st.session_state.last_access_time.strftime('%Y-%m-%d %H:%M:%S')}")
# 디버그 정보 (Debug info)
st.write("**All Session State:**")
st.json(dict(st.session_state))
6. 보안 고려사항 (Security Considerations)
🛡️ 보안 원칙 (Security Principles)
1. 최소 권한 원칙 (Principle of Least Privilege)
적용 사례:
기본 상태: 프리미엄 잠금 (premium_unlocked = False)
명시적 해제만 허용 (Explicit unlock only)
세션 종료 시 자동 잠금 (Auto-lock on session end)
# 기본 잠금 (Default locked)
if 'premium_unlocked' not in st.session_state:
st.session_state.premium_unlocked = False # 안전한 기본값
2. 심층 방어 (Defense in Depth)
다층 보안 (Multiple layers):
Layer 1: 환경 감지 (Environment Detection)
# 로컬 vs 클라우드 구분
if is_local_dev():
return True # 로컬 개발자만
Layer 2: 코드 형식 검증 (Format Validation)
# 정규표현식 검증
if not re.match(r'^PREM-[A-Z0-9]{4}-[A-Z0-9]{4}$', code):
return False
Layer 3: 코드 존재 확인 (Existence Check)
# Secrets 리스트에서 확인
if code not in valid_codes:
return False
Layer 4: 세션 상태 검증 (Session State Verification)
# 매 요청마다 재확인
if not st.session_state.premium_unlocked:
show_lock_screen()
3. 비밀 정보 보호 (Secret Protection)
금지 사항 (Do Not): ❌ 코드에 하드코딩 ❌ Git에 커밋 ❌ 로그에 출력 ❌ URL 파라미터 전달 ❌ 클라이언트 측 저장
권장 사항 (Recommended): ✅ Streamlit Secrets 사용 ✅ 환경 변수 활용 ✅ 서버 측 검증만 ✅ HTTPS 통신 ✅ 민감 정보 마스킹
# 시도 횟수 제한 (Limit attempts)
if st.session_state.access_attempts >= 10:
st.error("❌ Too many attempts. Please try again later.")
st.stop()
# 시도 간 지연 (Delay between attempts)
import time
time.sleep(1) # 1초 지연
2. 코드 유출 (Code Leakage)
위협: 코드가 공개 포럼에 유출
대응:
코드 1회 사용 후 만료 (One-time use)
사용자 식별 및 로깅
유출 시 즉시 코드 교체
# 사용 이력 기록 (Track usage)
used_codes = st.secrets.get("used_codes", [])
if code in used_codes:
return False, "Code already used"
# 중요 작업 전 재검증 (Re-verify before critical actions)
def run_backtesting():
# 서버 측 재검증 (Server-side re-verification)
if not verify_premium_status():
st.error("❌ Unauthorized access")
return
# 백테스팅 실행 (Run backtesting)
...
📝 감사 로그 (Audit Log)
접근 기록 (Access Logging)
import logging
from datetime import datetime
# 로거 설정 (Setup logger)
logging.basicConfig(
filename='premium_access.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def log_premium_access(code, success, ip=None):
"""프리미엄 접근 로그 (Log premium access)"""
masked_code = code[:8] + "****" if code else "None"
status = "SUCCESS" if success else "FAILURE"
logging.info(f"Premium access attempt: {status} | Code: {masked_code} | IP: {ip}")
# Streamlit Cloud에서는 st.write로 대체 가능
# (In Streamlit Cloud, can use st.write instead)
통계 대시보드 (Statistics Dashboard)
def show_access_statistics():
"""액세스 통계 표시 (Show access statistics)"""
st.subheader("📊 Access Statistics")
# 전체 시도 (Total attempts)
st.metric("Total Attempts", st.session_state.access_attempts)
# 성공률 (Success rate)
success_rate = 100 if st.session_state.premium_unlocked else 0
st.metric("Success Rate", f"{success_rate}%")
# 마지막 접근 (Last access)
if st.session_state.last_access_time:
st.metric("Last Access", st.session_state.last_access_time.strftime('%Y-%m-%d %H:%M:%S'))
✅ 보안 체크리스트 (Security Checklist)
배포 전 확인 사항 (Pre-deployment Checklist)
.streamlit/secrets.toml이 .gitignore에 있는가?
Streamlit Cloud Secrets가 설정되었는가?
로컬 환경 변수 LOTTO_DEV_MODE 제거했는가?
액세스 코드가 코드에 하드코딩되지 않았는가?
모든 검증이 서버 측에서 수행되는가?
에러 메시지가 민감 정보를 노출하지 않는가?
HTTPS가 활성화되어 있는가? (Streamlit Cloud 자동)
세션 타임아웃이 설정되었는가?
로그에 민감 정보가 포함되지 않는가?
프리미엄 기능이 올바르게 보호되는가?
코드 리뷰 항목 (Code Review Items)
# ✅ 올바른 예 (Correct)
if "premium" in st.secrets:
codes = st.secrets["premium"]["access_codes"]
# ❌ 잘못된 예 (Incorrect)
codes = ["PREM-A1B2-C3D4"] # 하드코딩 금지!
# ✅ 올바른 예 (Correct)
if normalized in valid_codes:
st.session_state.premium_unlocked = True
# ❌ 잘못된 예 (Incorrect)
st.session_state.premium_unlocked = True # 검증 없이!