Files
sam-manage/resources/views/academy/env-management.blade.php
김보곤 193cd2666f chore: [ai] Gemini 모델 gemini-2.0-flash → gemini-2.5-flash 마이그레이션
- config/services.php fallback 기본값 변경
- AiConfig DEFAULT_MODELS 상수 + getActiveGemini() fallback 변경
- NotionService fallback 변경
- AI 설정 관리 UI placeholder/기본값 변경
- Google Cloud AI 가이드 서비스 현황 모델명 변경
- 환경변수 관리 아카데미 예시 변경
2026-03-03 08:09:06 +09:00

1010 lines
65 KiB
PHP

@extends('layouts.app')
@section('title', '.env 관리 정책')
@push('styles')
<style>
/* 이미지 기본 스타일 */
.academy-img-hover {
transition: box-shadow 0.2s ease;
}
.academy-img-hover:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.academy-img-wrap {
overflow: hidden;
border-radius: 0.75rem;
}
/* hover 프리뷰 오버레이 */
#hover-preview {
display: none;
position: fixed;
inset: 0;
z-index: 45;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(3px);
pointer-events: none;
}
#hover-preview.is-active {
display: flex;
pointer-events: none;
}
#hover-preview img {
max-height: 80vh;
max-width: 85vw;
border-radius: 0.75rem;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
opacity: 0;
transform: scale(0.3);
transition: opacity 0.25s ease, transform 0.25s ease;
}
#hover-preview.is-active img {
opacity: 1;
transform: scale(1);
}
#hover-preview .hover-caption {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.85);
font-size: 0.8rem;
background: rgba(0,0,0,0.5);
padding: 0.4rem 1rem;
border-radius: 2rem;
white-space: nowrap;
opacity: 0;
transition: opacity 0.3s ease 0.15s;
}
#hover-preview.is-active .hover-caption {
opacity: 1;
}
/* 클릭 라이트박스 */
#lightbox {
display: none;
position: fixed;
inset: 0;
z-index: 50;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(4px);
}
#lightbox.is-open {
display: flex;
}
#lightbox img {
max-height: 90vh;
max-width: 90vw;
border-radius: 0.5rem;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}
</style>
@endpush
@section('content')
<div class="max-w-6xl mx-auto">
{{-- ============================================================ --}}
{{-- 히어로 배너 --}}
{{-- ============================================================ --}}
<div class="rounded-2xl overflow-hidden mb-8 shadow-lg" style="background: linear-gradient(135deg, #0d2d1f 0%, #0f172a 100%);">
<div class="flex items-center" style="flex-wrap: wrap;">
<div style="flex: 1 1 300px; padding: 2rem 2.5rem;">
<div class="flex items-center gap-2 text-sm mb-2" style="color: #34d399;">
<span>아카데미</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
<span style="color: #ffffff;">.env 관리 정책</span>
</div>
<h1 class="text-3xl font-bold mb-2" style="color: #ffffff;">.env 관리 정책</h1>
<p class="text-sm" style="color: #cbd5e1;">SAM 프로젝트의 환경 변수 관리 열쇠 고리처럼 서비스를 여는 비밀 설정 파일</p>
</div>
<div class="shrink-0" style="width: 240px; padding: 1.5rem;">
<div class="overflow-hidden rounded-xl">
<img src="{{ asset('images/academy/env-management/1.svg') }}" alt=".env 열쇠 고리 비유"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
</div>
</div>
</div>
<div class="flex gap-8">
{{-- ============================================================ --}}
{{-- 좌측 고정 목차 (TOC) --}}
{{-- ============================================================ --}}
<nav class="hidden lg:block shrink-0" style="width: 220px;">
<div class="sticky top-24">
<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-5">
<h2 class="font-semibold text-emerald-800 mb-3 flex items-center gap-2 text-sm">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
목차
</h2>
<div class="space-y-0.5 text-xs">
<a href="#env-what" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium">1. .env란 무엇인가?</a>
<a href="#env-analogy" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">열쇠 고리 비유</a>
<a href="#env-example" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">.env.example vs .env</a>
<a href="#env-structure" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">2. SAM의 .env 구조</a>
<a href="#env-mng" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">MNG 카테고리</a>
<a href="#env-api" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">API 카테고리</a>
<a href="#env-docker" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">3. Docker Override</a>
<a href="#env-priority" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">우선순위</a>
<a href="#env-override-example" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">실제 예시</a>
<a href="#env-sync" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">4. 동기화 필수 변수</a>
<a href="#env-sync-shared" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">공유 API </a>
<a href="#env-sync-path" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">경로가 다른 변수</a>
<a href="#env-local-server" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">5. 로컬 vs 서버</a>
<a href="#env-debug-danger" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">APP_DEBUG 위험</a>
<a href="#env-change" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">6. 변경 </a>
<a href="#env-cache" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">캐시 클리어</a>
<a href="#env-onboarding" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">온보딩 체크리스트</a>
<a href="#env-caution" class="block text-emerald-700 hover:text-emerald-900 py-1 font-medium mt-2">7. 주의사항 & 실수</a>
<a href="#env-forbidden" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">금지 카드</a>
<a href="#env-troubleshoot" class="block text-emerald-600 hover:text-emerald-800 py-0.5 pl-3">증상별 해결</a>
</div>
</div>
</div>
</nav>
{{-- ============================================================ --}}
{{-- 우측 콘텐츠 --}}
{{-- ============================================================ --}}
<div class="flex-1 min-w-0 space-y-10">
{{-- 모바일 목차 --}}
<div class="lg:hidden bg-emerald-50 border border-emerald-200 rounded-xl p-4 mb-6">
<details>
<summary class="font-semibold text-emerald-800 text-sm cursor-pointer flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
목차 보기
</summary>
<nav class="mt-3 space-y-1 text-sm">
<a href="#env-what" class="block text-emerald-700 hover:text-emerald-900 py-1">1. .env란 무엇인가?</a>
<a href="#env-structure" class="block text-emerald-700 hover:text-emerald-900 py-1">2. SAM의 .env 구조</a>
<a href="#env-docker" class="block text-emerald-700 hover:text-emerald-900 py-1">3. Docker가 .env를 덮어쓴다</a>
<a href="#env-sync" class="block text-emerald-700 hover:text-emerald-900 py-1">4. 양쪽이 같아야 하는 변수</a>
<a href="#env-local-server" class="block text-emerald-700 hover:text-emerald-900 py-1">5. 로컬 vs 서버 환경</a>
<a href="#env-change" class="block text-emerald-700 hover:text-emerald-900 py-1">6. .env 변경 해야 </a>
<a href="#env-caution" class="block text-emerald-700 hover:text-emerald-900 py-1">7. 주의사항 & 자주 하는 실수</a>
</nav>
</details>
</div>
{{-- ============================================================ --}}
{{-- 1. .env란 무엇인가? --}}
{{-- ============================================================ --}}
<section id="env-what" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">1</span>
.env란 무엇인가?
</h2>
<!-- 1-1. 열쇠 고리 비유 -->
<div id="env-analogy" class="scroll-mt-20 mb-8">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
열쇠 고리 비유
</h3>
<div class="bg-emerald-50 rounded-lg p-5 border border-emerald-100 mb-4">
<p class="text-sm text-emerald-900 leading-relaxed">
<strong>.env 파일</strong> <strong>열쇠 고리</strong> 같다.
데이터베이스, 메일, AI, 푸시 알림 여러 서비스를 여는 열쇠(비밀번호, API ) 곳에 모아둔 파일이다.
</p>
</div>
<div class="bg-amber-50 rounded-lg p-4 border border-amber-100 mb-4">
<p class="font-semibold text-amber-800 mb-2"> 설정을 코드에서 분리하는가?</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div class="bg-white rounded-lg p-3 border border-amber-200">
<p class="font-bold text-amber-800 mb-1">보안</p>
<p class="text-amber-900">API , 비밀번호를 코드에 직접 적으면 Git에 올라가서 유출된다. .env는 Git에 올리지 않는다.</p>
</div>
<div class="bg-white rounded-lg p-3 border border-amber-200">
<p class="font-bold text-amber-800 mb-1">환경별 차이</p>
<p class="text-amber-900">로컬에서는 DB가 Docker 컨테이너, 서버에서는 localhost. 코드는 같은데 설정만 다르다.</p>
</div>
</div>
</div>
</div>
<!-- 1-2. .env.example vs .env -->
<div id="env-example" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
.env.example vs .env
</h3>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 mb-4">
<p class="font-semibold text-gray-800 mb-2">비유: 양식과 실제 서류</p>
<p class="text-xs text-gray-700 leading-relaxed mb-3">
<code>.env.example</code> <strong> 양식지</strong>. 어떤 항목을 채워야 하는지 알려주지만 실제 값은 비어 있다.<br>
<code>.env</code> <strong>작성 완료된 서류</strong>. 실제 비밀번호와 API 키가 들어 있다.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div class="bg-white rounded-lg p-3 border border-gray-300">
<p class="font-bold text-gray-800 mb-2">.env.example (양식)</p>
<code class="block bg-gray-100 rounded p-2 text-[11px] leading-relaxed">
GEMINI_API_KEY=<br>
DB_PASSWORD=sampass<br>
INTERNAL_EXCHANGE_SECRET=
</code>
<p class="mt-2 text-gray-600">Git에 포함 (공유용)</p>
</div>
<div class="bg-white rounded-lg p-3 border border-emerald-300">
<p class="font-bold text-emerald-800 mb-2">.env (실제)</p>
<code class="block bg-gray-100 rounded p-2 text-[11px] leading-relaxed">
GEMINI_API_KEY=AIzaSy...<br>
DB_PASSWORD=Pr0d_S3cur3!<br>
INTERNAL_EXCHANGE_SECRET=abc123...
</code>
<p class="mt-2 text-emerald-700 font-semibold">Git에 절대 포함 금지!</p>
</div>
</div>
</div>
<!-- SVG 2 -->
<div class="overflow-hidden rounded-xl mb-4">
<img src="{{ asset('images/academy/env-management/2.svg') }}" alt=".env 역할 개념도"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 2. SAM 프로젝트의 .env 구조 --}}
{{-- ============================================================ --}}
<section id="env-structure" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">2</span>
SAM 프로젝트의 .env 구조
</h2>
<div class="bg-emerald-50 rounded-lg p-5 border border-emerald-100 mb-6">
<p class="text-sm text-emerald-900 leading-relaxed">
SAM은 <strong>MNG, API, React</strong> 3 프로젝트로 구성되며, 프로젝트가 <strong>독립된 .env 파일</strong> 보유한다.
공유 DB를 사용하지만 환경 변수는 각자 관리한다.
</p>
</div>
<!-- SVG 3 -->
<div class="overflow-hidden rounded-xl mb-6">
<img src="{{ asset('images/academy/env-management/3.svg') }}" alt="프로젝트별 .env 구조"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<!-- MNG 카테고리 -->
<div id="env-mng" class="scroll-mt-20 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
MNG .env 카테고리
</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 text-center">
<p class="font-bold text-gray-800">APP</p>
<p class="text-gray-600">이름, 환경, URL</p>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 text-center">
<p class="font-bold text-gray-800">Database</p>
<p class="text-gray-600">DB 접속 정보</p>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 text-center">
<p class="font-bold text-gray-800">Session</p>
<p class="text-gray-600">세션 드라이버, 수명</p>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 text-center">
<p class="font-bold text-gray-800">Mail</p>
<p class="text-gray-600">SMTP 서버 설정</p>
</div>
<div class="bg-purple-50 rounded-lg p-3 border border-purple-200 text-center">
<p class="font-bold text-purple-800">SAM API 연동</p>
<p class="text-purple-600">API_BASE_URL, 내부통신키</p>
</div>
<div class="bg-purple-50 rounded-lg p-3 border border-purple-200 text-center">
<p class="font-bold text-purple-800">Google AI</p>
<p class="text-purple-600">Gemini, Vertex, GCS</p>
</div>
<div class="bg-purple-50 rounded-lg p-3 border border-purple-200 text-center">
<p class="font-bold text-purple-800">Claude AI</p>
<p class="text-purple-600">CLAUDE_API_KEY</p>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 text-center">
<p class="font-bold text-gray-800">FCM</p>
<p class="text-gray-600">Firebase 푸시 알림</p>
</div>
<div class="bg-blue-50 rounded-lg p-3 border border-blue-200 text-center">
<p class="font-bold text-blue-800">Notion</p>
<p class="text-blue-600">MNG 전용</p>
</div>
<div class="bg-blue-50 rounded-lg p-3 border border-blue-200 text-center">
<p class="font-bold text-blue-800">기상청 API</p>
<p class="text-blue-600">MNG 전용</p>
</div>
</div>
<p class="text-xs text-gray-500 mt-2"><span class="inline-block w-2 h-2 bg-purple-400 rounded-full mr-1"></span>보라 = 양쪽 공유 · <span class="inline-block w-2 h-2 bg-blue-400 rounded-full mr-1"></span>파랑 = MNG 전용</p>
</div>
<!-- API 카테고리 -->
<div id="env-api" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
API .env 추가 카테고리
</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<div class="bg-amber-50 rounded-lg p-3 border border-amber-200 text-center">
<p class="font-bold text-amber-800">Slack 로깅</p>
<p class="text-amber-600">API 전용</p>
</div>
<div class="bg-amber-50 rounded-lg p-3 border border-amber-200 text-center">
<p class="font-bold text-amber-800">Swagger</p>
<p class="text-amber-600">API 문서 설정</p>
</div>
<div class="bg-amber-50 rounded-lg p-3 border border-amber-200 text-center">
<p class="font-bold text-amber-800">Sanctum</p>
<p class="text-amber-600">토큰 만료 설정</p>
</div>
<div class="bg-amber-50 rounded-lg p-3 border border-amber-200 text-center">
<p class="font-bold text-amber-800">Legacy DB</p>
<p class="text-amber-600">5130 DB 접속</p>
</div>
<div class="bg-amber-50 rounded-lg p-3 border border-amber-200 text-center">
<p class="font-bold text-amber-800">바로빌</p>
<p class="text-amber-600">세금계산서 SOAP</p>
</div>
</div>
<p class="text-xs text-gray-500 mt-2"><span class="inline-block w-2 h-2 bg-amber-400 rounded-full mr-1"></span>주황 = API 전용</p>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 3. Docker가 .env를 덮어쓴다 --}}
{{-- ============================================================ --}}
<section id="env-docker" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">3</span>
Docker가 .env를 덮어쓴다
</h2>
<div class="bg-amber-50 rounded-lg p-4 border border-amber-100 mb-6">
<p class="font-semibold text-amber-800 mb-2">비유: 현장 상관의 즉각 명령</p>
<p class="text-xs text-amber-900 leading-relaxed">
군대에서 <strong>기본 명령서</strong>(.env) 있지만, <strong>현장 상관</strong>(docker-compose) "이건 이렇게 해!"라고 하면 그게 우선이다.
Docker 환경에서는 <code>docker-compose.yml</code> <code>environment</code> 설정이 .env보다 강하다.
</p>
</div>
<!-- 우선순위 -->
<div id="env-priority" class="scroll-mt-20 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
우선순위 (높은 )
</h3>
<!-- SVG 4 -->
<div class="overflow-hidden rounded-xl mb-4">
<img src="{{ asset('images/academy/env-management/4.svg') }}" alt="Override 우선순위"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<div class="space-y-2 text-xs">
<div class="flex items-center gap-3 bg-emerald-50 rounded-lg p-3 border border-emerald-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">1</span>
<div>
<p class="font-bold text-emerald-800">docker-compose.yml <code>environment:</code></p>
<p class="text-emerald-700">최우선. 컨테이너 시작 직접 주입된다.</p>
</div>
</div>
<div class="flex items-center gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-gray-500 text-white rounded-full flex items-center justify-center text-xs font-bold">2</span>
<div>
<p class="font-bold text-gray-800">.env 파일</p>
<p class="text-gray-600">프로젝트 루트의 환경 변수 파일.</p>
</div>
</div>
<div class="flex items-center gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-gray-400 text-white rounded-full flex items-center justify-center text-xs font-bold">3</span>
<div>
<p class="font-bold text-gray-700">.env.example</p>
<p class="text-gray-500">기본값 참고용. 실제 적용되지 않는다.</p>
</div>
</div>
</div>
</div>
<!-- Override 실제 예시 -->
<div id="env-override-example" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
실제 Override 예시
</h3>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 mb-4">
<p class="font-semibold text-gray-800 mb-3">API 프로젝트의 DB_HOST</p>
<div class="space-y-2 text-xs">
<div class="flex items-center gap-2">
<span class="text-gray-400 line-through">api/.env: <code>DB_HOST=127.0.0.1</code></span>
<span class="text-red-500 text-[10px]">무시됨</span>
</div>
<div class="flex items-center gap-2">
<span class="text-emerald-700 font-bold">docker-compose: <code>DB_HOST=sam-mysql-1</code></span>
<span class="bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded text-[10px] font-bold">적용됨</span>
</div>
</div>
<div class="mt-3 bg-amber-50 rounded p-3 border border-amber-200">
<p class="text-xs text-amber-800">
<strong>Docker에서 Override하는 5 변수:</strong>
<code>DB_HOST</code>, <code>DB_PORT</code>, <code>DB_DATABASE</code>, <code>DB_USERNAME</code>, <code>DB_PASSWORD</code>
</p>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<p class="font-semibold text-gray-800 mb-3">React 프로젝트</p>
<div class="text-xs space-y-1">
<p class="text-gray-700"><code>NEXT_PUBLIC_API_URL=https://api.sam.kr</code></p>
<p class="text-gray-700"><code>NEXT_PUBLIC_API_KEY=42Jfwc6E...</code></p>
<p class="text-gray-700"><code>NODE_ENV=development</code></p>
</div>
<p class="text-xs text-gray-500 mt-2">React는 .env 파일 없이 docker-compose에서만 설정한다.</p>
</div>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 4. 양쪽이 같아야 하는 변수 --}}
{{-- ============================================================ --}}
<section id="env-sync" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">4</span>
양쪽이 같아야 하는 변수
</h2>
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200 mb-6">
<p class="font-semibold text-purple-800 mb-2">비유: 쌍의 자물쇠</p>
<p class="text-xs text-purple-900 leading-relaxed">
MNG와 API는 서로 HTTP로 통신한다. <strong>INTERNAL_EXCHANGE_SECRET</strong> 양쪽에서 다르면
HMAC 인증이 실패하여 통신이 불가능하다. 자물쇠와 열쇠가 맞지 않는 것과 같다.
</p>
</div>
<!-- SVG 5 -->
<div class="overflow-hidden rounded-xl mb-6">
<img src="{{ asset('images/academy/env-management/5.svg') }}" alt="동기화 필수 변수 맵"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<!-- 공유 API -->
<div id="env-sync-shared" class="scroll-mt-20 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
공유 API (양쪽 동일 필수)
</h3>
<div class="overflow-x-auto">
<table class="w-full text-xs border border-gray-200 rounded-lg overflow-hidden">
<thead class="bg-emerald-50">
<tr>
<th class="text-left p-2 font-semibold text-emerald-800 border-b">환경 변수</th>
<th class="text-center p-2 font-semibold text-emerald-800 border-b">MNG</th>
<th class="text-center p-2 font-semibold text-emerald-800 border-b">API</th>
<th class="text-left p-2 font-semibold text-emerald-800 border-b">설명</th>
</tr>
</thead>
<tbody>
<tr class="bg-red-50">
<td class="p-2 border-b font-mono font-bold text-red-800">INTERNAL_EXCHANGE_SECRET</td>
<td class="p-2 border-b text-center text-red-600 font-bold">필수</td>
<td class="p-2 border-b text-center text-red-600 font-bold">필수</td>
<td class="p-2 border-b text-red-700">서버 HMAC 검증 </td>
</tr>
<tr>
<td class="p-2 border-b font-mono">GEMINI_API_KEY</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-gray-600">Gemini AI API </td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 border-b font-mono">GEMINI_MODEL</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-gray-600">gemini-2.5-flash</td>
</tr>
<tr>
<td class="p-2 border-b font-mono">VERTEX_AI_PROJECT_ID</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-gray-600">Vertex AI 프로젝트</td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 border-b font-mono">VERTEX_AI_LOCATION</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-gray-600">us-central1</td>
</tr>
<tr>
<td class="p-2 border-b font-mono">GOOGLE_STORAGE_BUCKET</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-center text-emerald-600">&#10003;</td>
<td class="p-2 border-b text-gray-600">GCS 버킷 이름</td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 font-mono">DB_HOST / DB_DATABASE / DB_PASSWORD</td>
<td class="p-2 text-center text-emerald-600">&#10003;</td>
<td class="p-2 text-center text-emerald-600">&#10003;</td>
<td class="p-2 text-gray-600">공유 DB 접속 정보</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 경로가 다른 변수 -->
<div id="env-sync-path" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
경로가 다른 변수 (같은 파일, 다른 경로)
</h3>
<!-- SVG 6 -->
<div class="overflow-hidden rounded-xl mb-4">
<img src="{{ asset('images/academy/env-management/6.svg') }}" alt="경로가 다른 변수"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs border border-gray-200 rounded-lg overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="text-left p-2 font-semibold text-gray-800 border-b">환경 변수</th>
<th class="text-left p-2 font-semibold text-gray-800 border-b">MNG 경로</th>
<th class="text-left p-2 font-semibold text-gray-800 border-b">API 경로</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-2 border-b font-mono font-semibold">GOOGLE_APPLICATION_CREDENTIALS</td>
<td class="p-2 border-b font-mono text-[10px]">/var/www/sales/apikey/google_sa.json</td>
<td class="p-2 border-b font-mono text-[10px]">/var/www/mng/apikey/google_sa.json</td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 font-mono font-semibold">FCM_SA_PATH</td>
<td class="p-2 font-mono text-[10px]">secrets/firebase-service-account.json</td>
<td class="p-2 font-mono text-[10px]">secrets/codebridge-x-firebase-sa.json</td>
</tr>
</tbody>
</table>
</div>
<div class="bg-amber-50 rounded-lg p-3 border border-amber-100 mt-3">
<p class="text-xs text-amber-800">
동일한 Google 서비스 계정 파일이지만 컨테이너마다 마운트 경로가 다르다.
값을 복사하면 되고 프로젝트의 실제 경로를 사용해야 한다.
</p>
</div>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 5. 로컬 vs 서버 환경 --}}
{{-- ============================================================ --}}
<section id="env-local-server" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">5</span>
로컬 vs 서버 환경
</h2>
<!-- SVG 7 -->
<div class="overflow-hidden rounded-xl mb-6">
<img src="{{ asset('images/academy/env-management/7.svg') }}" alt="로컬 vs 서버 비교"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<div class="overflow-x-auto mb-6">
<table class="w-full text-xs border border-gray-200 rounded-lg overflow-hidden">
<thead class="bg-emerald-50">
<tr>
<th class="text-left p-2 font-semibold text-emerald-800 border-b">환경 변수</th>
<th class="text-left p-2 font-semibold text-emerald-800 border-b">로컬 (Docker)</th>
<th class="text-left p-2 font-semibold text-emerald-800 border-b">서버 (운영)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-2 border-b font-mono font-semibold">APP_ENV</td>
<td class="p-2 border-b"><code>local</code></td>
<td class="p-2 border-b"><code>production</code></td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 border-b font-mono font-semibold">APP_DEBUG</td>
<td class="p-2 border-b"><code class="text-emerald-700">true</code></td>
<td class="p-2 border-b"><code class="text-red-700 font-bold">false</code></td>
</tr>
<tr>
<td class="p-2 border-b font-mono font-semibold">DB_HOST</td>
<td class="p-2 border-b"><code>sam-mysql-1</code> (컨테이너명)</td>
<td class="p-2 border-b"><code>127.0.0.1</code> (localhost)</td>
</tr>
<tr class="bg-gray-50">
<td class="p-2 font-mono font-semibold">DB_PASSWORD</td>
<td class="p-2"><code>sampass</code> (개발용)</td>
<td class="p-2"><code>●●●●●●●</code> (강력한 비밀번호)</td>
</tr>
</tbody>
</table>
</div>
<!-- APP_DEBUG 위험 -->
<div id="env-debug-danger" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
APP_DEBUG=true의 운영 환경 위험성
</h3>
<div class="bg-red-50 rounded-lg p-4 border border-red-200">
<p class="font-bold text-red-800 mb-2">운영 서버에서 APP_DEBUG=true이면?</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
<div class="bg-white rounded-lg p-3 border border-red-200 text-center">
<p class="font-bold text-red-800 mb-1">DB 비밀번호 노출</p>
<p class="text-red-700">에러 발생 .env 값이<br>브라우저에 표시된다</p>
</div>
<div class="bg-white rounded-lg p-3 border border-red-200 text-center">
<p class="font-bold text-red-800 mb-1">API 유출</p>
<p class="text-red-700">스택 트레이스에 환경 변수<br>전체가 포함될 있다</p>
</div>
<div class="bg-white rounded-lg p-3 border border-red-200 text-center">
<p class="font-bold text-red-800 mb-1">서버 경로 노출</p>
<p class="text-red-700">파일 구조, 프레임워크 버전 <br>공격에 필요한 정보가 드러난다</p>
</div>
</div>
</div>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 6. .env 변경 해야 --}}
{{-- ============================================================ --}}
<section id="env-change" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">6</span>
.env 변경 해야
</h2>
<!-- 캐시 메커니즘 설명 -->
<div id="env-cache" class="scroll-mt-20 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
config:clear가 필요한가
</h3>
<div class="bg-emerald-50 rounded-lg p-5 border border-emerald-100 mb-4">
<p class="text-sm text-emerald-900 leading-relaxed">
Laravel은 성능을 위해 <code>.env</code> 값을 <strong>캐시</strong> 저장해둔다.
.env를 수정해도 <code>php artisan config:clear</code> 실행하지 않으면 <strong>이전 값이 계속 사용</strong>된다.
</p>
</div>
<!-- SVG 8 -->
<div class="overflow-hidden rounded-xl mb-4">
<img src="{{ asset('images/academy/env-management/8.svg') }}" alt=".env 변경 → 캐시 클리어 흐름"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<p class="font-bold text-gray-800 mb-2">로컬 (Docker) 환경</p>
<div class="space-y-2">
<div class="bg-white rounded p-2 border border-gray-300">
<code class="text-[11px]">docker exec sam-mng-1 php artisan config:clear</code>
</div>
<div class="bg-white rounded p-2 border border-gray-300">
<code class="text-[11px]">docker exec sam-api-1 php artisan config:clear</code>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<p class="font-bold text-gray-800 mb-2">서버 환경</p>
<div class="space-y-2">
<div class="bg-white rounded p-2 border border-gray-300">
<code class="text-[11px]">cd /home/webservice/mng && php artisan config:clear</code>
</div>
<div class="bg-white rounded p-2 border border-gray-300">
<code class="text-[11px]">cd /home/webservice/api && php artisan config:clear</code>
</div>
</div>
</div>
</div>
</div>
<!-- 온보딩 체크리스트 -->
<div id="env-onboarding" class="scroll-mt-20">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
신규 개발자 온보딩 체크리스트
</h3>
<!-- SVG 9 -->
<div class="overflow-hidden rounded-xl mb-4">
<img src="{{ asset('images/academy/env-management/9.svg') }}" alt="온보딩 체크리스트"
class="w-full rounded-xl cursor-pointer academy-img-hover"
onclick="openLightbox(this)">
</div>
<div class="space-y-2 text-xs">
<div class="flex items-start gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">1</span>
<div>
<p class="font-bold text-gray-800">.env.example을 .env로 복사</p>
<code class="text-[11px] text-gray-600">cp .env.example .env</code>
</div>
</div>
<div class="flex items-start gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">2</span>
<div>
<p class="font-bold text-gray-800">APP_KEY 생성</p>
<code class="text-[11px] text-gray-600">docker exec sam-mng-1 php artisan key:generate</code>
</div>
</div>
<div class="flex items-start gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">3</span>
<div>
<p class="font-bold text-gray-800">DB 접속 정보 확인</p>
<p class="text-gray-600">Docker가 Override하므로 .env 기본값으로 충분하다</p>
</div>
</div>
<div class="flex items-start gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">4</span>
<div>
<p class="font-bold text-gray-800">공유 API 동기화</p>
<p class="text-gray-600">GEMINI_API_KEY, INTERNAL_EXCHANGE_SECRET 등을 팀장에게 받아 설정</p>
</div>
</div>
<div class="flex items-start gap-3 bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="shrink-0 w-6 h-6 bg-emerald-500 text-white rounded-full flex items-center justify-center text-xs font-bold">5</span>
<div>
<p class="font-bold text-gray-800">캐시 초기화</p>
<code class="text-[11px] text-gray-600">docker exec sam-mng-1 php artisan config:clear</code>
</div>
</div>
</div>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 7. 주의사항 & 자주 하는 실수 --}}
{{-- ============================================================ --}}
<section id="env-caution" class="scroll-mt-20">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-3">
<span class="w-8 h-8 bg-emerald-500 text-white rounded-lg flex items-center justify-center text-sm font-bold">7</span>
주의사항 & 자주 하는 실수
</h2>
<!-- 금지 카드 6 -->
<div id="env-forbidden" class="scroll-mt-20 mb-8">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
절대 하지 마라 (6가지)
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">.env를 Git에 커밋</p>
<p class="text-red-700">비밀번호, API 키가 저장소에 영구 저장된다. .gitignore에 .env가 포함되어 있는지 확인한다.</p>
</div>
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">운영 서버에서 APP_DEBUG=true</p>
<p class="text-red-700">에러 발생 모든 환경 변수가 브라우저에 노출된다.</p>
</div>
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">EXCHANGE_SECRET 불일치</p>
<p class="text-red-700">MNG와 API의 INTERNAL_EXCHANGE_SECRET이 다르면 서버 통신이 전부 실패한다.</p>
</div>
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">운영 서버에서 APP_KEY 재생성</p>
<p class="text-red-700">기존 세션, 암호화된 데이터가 모두 복호화 불가능해진다.</p>
</div>
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">config:clear 하고 "안 되요"</p>
<p class="text-red-700">.env 수정 캐시를 지우면 이전 설정이 계속 적용된다.</p>
</div>
<div class="bg-red-50 rounded-lg p-3 border border-red-200">
<p class="font-bold text-red-800 mb-1">운영 키를 로컬에 복사</p>
<p class="text-red-700">로컬에서 운영 DB에 접속하면 실수로 데이터를 변경할 있다.</p>
</div>
</div>
</div>
<!-- 증상별 해결책 -->
<div id="env-troubleshoot" class="scroll-mt-20 mb-8">
<h3 class="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
증상별 해결책
</h3>
<div class="space-y-2 text-xs">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex items-start gap-3">
<span class="shrink-0 bg-red-100 text-red-800 px-2 py-0.5 rounded font-bold text-[10px]">증상</span>
<div>
<p class="font-bold text-gray-800">MNG API 연동 실패 (401/403)</p>
<p class="text-gray-600 mt-1">
<span class="bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-[10px] font-bold">해결</span>
양쪽 <code>INTERNAL_EXCHANGE_SECRET</code> 일치 확인 <code>config:clear</code>
</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex items-start gap-3">
<span class="shrink-0 bg-red-100 text-red-800 px-2 py-0.5 rounded font-bold text-[10px]">증상</span>
<div>
<p class="font-bold text-gray-800">AI 기능 (Gemini/Claude) 동작 </p>
<p class="text-gray-600 mt-1">
<span class="bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-[10px] font-bold">해결</span>
해당 프로젝트 .env의 <code>GEMINI_API_KEY</code> 또는 <code>CLAUDE_API_KEY</code> 확인 <code>config:clear</code>
</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex items-start gap-3">
<span class="shrink-0 bg-red-100 text-red-800 px-2 py-0.5 rounded font-bold text-[10px]">증상</span>
<div>
<p class="font-bold text-gray-800">DB 접속 오류 (Connection refused)</p>
<p class="text-gray-600 mt-1">
<span class="bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-[10px] font-bold">해결</span>
Docker: <code>DB_HOST=sam-mysql-1</code> / 서버: <code>DB_HOST=127.0.0.1</code> 확인. Docker에서는 compose가 override하므로 .env 값은 무관.
</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex items-start gap-3">
<span class="shrink-0 bg-red-100 text-red-800 px-2 py-0.5 rounded font-bold text-[10px]">증상</span>
<div>
<p class="font-bold text-gray-800">Google 서비스 계정 파일 오류</p>
<p class="text-gray-600 mt-1">
<span class="bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-[10px] font-bold">해결</span>
<code>GOOGLE_APPLICATION_CREDENTIALS</code> 경로가 해당 컨테이너의 마운트 경로와 일치하는지 확인.
MNG와 API의 경로가 다르다.
</p>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<div class="flex items-start gap-3">
<span class="shrink-0 bg-red-100 text-red-800 px-2 py-0.5 rounded font-bold text-[10px]">증상</span>
<div>
<p class="font-bold text-gray-800">.env 수정했는데 설정이 바뀜</p>
<p class="text-gray-600 mt-1">
<span class="bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-[10px] font-bold">해결</span>
<code>php artisan config:clear</code> 실행. Docker의 override 변수인지도 확인 (compose가 .env보다 우선).
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 신규 변수 추가 절차 -->
<div class="bg-emerald-50 rounded-lg p-4 border border-emerald-200">
<p class="font-semibold text-emerald-800 mb-2">신규 환경 변수 추가 절차</p>
<ol class="text-xs text-emerald-900 space-y-1 list-decimal list-inside">
<li><code>.env.example</code> =기본값 추가 (다른 개발자를 위한 문서 역할)</li>
<li>자신의 <code>.env</code> 실제 설정</li>
<li><code>config/services.php</code> 등에서 <code>env('NEW_KEY')</code> 참조</li>
<li>팀원에게 변수 추가 사실 공유</li>
<li>양쪽(MNG/API) 필요 동기화 필수 항목인지 확인</li>
</ol>
</div>
</div>
</section>
{{-- ============================================================ --}}
{{-- 용어 사전 --}}
{{-- ============================================================ --}}
@include('components.academy-glossary', ['domain' => 'env-management'])
</div>
</div>
</div>
<!-- hover 프리뷰 오버레이 -->
<div id="hover-preview">
<img id="hover-preview-img" src="" alt="">
<span class="hover-caption" id="hover-preview-caption"></span>
</div>
<!-- 클릭 라이트박스 -->
<div id="lightbox" onclick="closeLightbox()">
<button onclick="closeLightbox()" style="position:absolute; top:1rem; right:1rem; background:none; border:none; cursor:pointer; color:rgba(255,255,255,0.8); padding:0.5rem;">
<svg style="width:2rem; height:2rem;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<img id="lightbox-img" onclick="event.stopPropagation()">
</div>
<script>
(function() {
var preview = document.getElementById('hover-preview');
var previewImg = document.getElementById('hover-preview-img');
var previewCaption = document.getElementById('hover-preview-caption');
var hoverTimer = null;
var isPreviewActive = false;
var HOVER_DELAY = 350;
document.querySelectorAll('.academy-img-hover').forEach(function(img) {
img.addEventListener('mouseenter', function() {
var el = this;
hoverTimer = setTimeout(function() {
showPreview(el);
}, HOVER_DELAY);
});
img.addEventListener('mouseleave', function() {
clearTimeout(hoverTimer);
if (isPreviewActive) {
hidePreview();
}
});
});
function showPreview(el) {
previewImg.src = el.src;
previewImg.alt = el.alt || '';
var caption = el.alt || '';
var nextP = el.parentElement && el.parentElement.querySelector('p');
if (nextP) caption = nextP.textContent;
previewCaption.textContent = caption;
preview.classList.add('is-active');
isPreviewActive = true;
}
function hidePreview() {
preview.classList.remove('is-active');
isPreviewActive = false;
}
window.openLightbox = function(el) {
var lb = document.getElementById('lightbox');
var img = document.getElementById('lightbox-img');
img.src = el.src;
img.alt = el.alt;
lb.classList.add('is-open');
document.body.style.overflow = 'hidden';
hidePreview();
};
window.closeLightbox = function() {
var lb = document.getElementById('lightbox');
lb.classList.remove('is-open');
document.body.style.overflow = '';
};
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (isPreviewActive) hidePreview();
closeLightbox();
}
});
})();
</script>
@endsection