# SNAP 인증 통합 — 커밋 메시지 및 변경 내역 --- ## 커밋 메시지 ``` feat: LDAP 인증 통합 — 로그인 게이팅 + HttpOnly 쿠키 + UserMenu SK실트론 사내 AD(Active Directory) 기반 LDAP 인증을 SNAP 웹 UI에 통합. BFF(Express)가 인증 서버(10.150.6.47:8090)를 프록시하여 HttpOnly 쿠키로 JWT를 관리하는 구조. ## 주요 변경 ### server.ts — BFF 인증 라우트 추가 - POST /api/auth/login: 인증 서버 프록시 + HttpOnly 쿠키 Set-Cookie - POST /api/auth/logout: 쿠키 삭제 + 인증 서버 블랙리스트 등록 - cookie-parser 미들웨어 추가 - AUTH_API_BASE 환경변수 (기본: http://10.150.6.47:8090) ### src/App.tsx — 인증 UI 컴포넌트 추가 (1326줄 → 1657줄) - AuthProvider: 전역 인증 상태 (login/logout/user) + sessionStorage 동기화 - LoginScreen: 전체화면 로그인 UI (사번/비밀번호, SK 디자인 토큰 적용) - UserMenu: Navbar 우측 사용자 아바타 + 이름 + 드롭다운(부서/직책/로그아웃) - AuthGate: 로그인 필수 게이팅 (비로그인 → LoginScreen, 로그인 → AppContent) - 기존 App() → AppContent()로 리네임, 새 App()이 AuthProvider 래퍼 ### src/types.ts — AuthUser 인터페이스 추가 - name, username, department, company, email, section, title 필드 - 인증 서버 /login 응답 구조와 1:1 매핑 ## 인증 흐름 1. 사용자 접속 → AuthGate가 sessionStorage 확인 2. 비로그인 → LoginScreen 표시 3. 사번/비밀번호 입력 → POST /api/auth/login 4. BFF → 인증 서버(form-urlencoded) → LDAP bind 5. 성공 → BFF가 JWT를 HttpOnly 쿠키로 set + 사용자 정보를 JSON 반환 6. React가 sessionStorage에 사용자 정보 저장 → AppContent 표시 7. 로그아웃 → 쿠키 삭제 + 인증 서버 블랙리스트 등록 → LoginScreen 복귀 ## 보안 - JWT는 HttpOnly 쿠키로만 보관 (XSS 방어) - 토큰이 React 코드에 노출되지 않음 - 인증 서버 IP가 클라이언트에 노출되지 않음 (BFF 프록시) - SameSite=Lax (CSRF 기본 방어) - sessionStorage: 탭 종료 시 세션 만료 ## 의존성 추가 - cookie-parser - @types/cookie-parser (devDependencies) ## 환경변수 추가 - AUTH_API_BASE: 인증 서버 URL (기본: http://10.150.6.47:8090) ``` --- ## 변경 파일 목록 | 파일 | 상태 | 변경 내용 | |------|------|-----------| | `server.ts` | Modified | cookie-parser + 인증 라우트 2개 추가 | | `src/App.tsx` | Modified | AuthProvider, LoginScreen, UserMenu, AuthGate 추가 | | `src/types.ts` | Modified | AuthUser 인터페이스 추가 | | `package.json` | Modified | cookie-parser 의존성 추가 | --- ## Git 명령 ```bash # 1. 패키지 설치 (이미 했다면 생략) npm install cookie-parser npm install -D @types/cookie-parser # 2. 스테이징 git add server.ts src/App.tsx src/types.ts package.json package-lock.json # 3. 커밋 git commit -m "feat: LDAP 인증 통합 — 로그인 게이팅 + HttpOnly 쿠키 + UserMenu" # 4. 푸시 git push origin main ``` --- ## 아키텍처 다이어그램 ``` ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ ┌──────────┐ │ Browser │ │ Express BFF │ │ Auth Server │ │ AD/LDAP │ │ (React) │ │ (server.ts) │ │ (Starlette) │ │ │ │ │ │ │ │ │ │ │ │ LoginScreen │─────►│ /api/auth/ │─────►│ /login │─────►│ LDAP │ │ 사번+비번 │ POST │ login │ POST │ │ bind │ bind │ │ │ │ │ │ │ │ │ │ │◄─────│ Set-Cookie: │◄─────│ {user+token} │◄─────│ 인증결과 │ │ sessionStor │ JSON │ snap_auth=JWT │ │ │ │ │ │ age 저장 │(user)│ (HttpOnly) │ │ │ │ │ │ │ │ │ │ │ │ │ │ UserMenu │ │ │ │ │ │ │ │ 이름+부서 │ │ │ │ │ │ │ │ 로그아웃 │─────►│ /api/auth/ │─────►│ /logout │ │ │ │ │ POST │ logout │ POST │ Redis 블랙 │ │ │ │ │◄─────│ 쿠키 삭제 │ │ 리스트 등록 │ │ │ │ │ │ │ │ │ │ │ │ 검색 요청 │─────►│ /api/search │─────►│ │ │ │ │ (쿠키 자동) │ │ (기존 동일) │ │ qp-backend │ │ │ └──────────────┘ └────────────────┘ └──────────────┘ └──────────┘ ``` --- ## 컴포넌트 구조 (App.tsx) ``` App (export default) └─ AuthProvider (전역 인증 상태) └─ AuthGate (게이팅 분기) ├─ isLoading → 스피너 ├─ !user → LoginScreen (전체화면 로그인) └─ user → AppContent (기존 SNAP UI) ├─ Navbar │ ├─ SNAP 로고 + SONI/QMS 링크 │ └─ UserMenu (사용자 아바타 + 드롭다운) ├─ 검색 화면 (view: 'search') ├─ 결과 화면 (view: 'results') └─ DetailModal (상세 모달) ``` --- ## 환경변수 전체 목록 (BFF) | 변수 | 기본값 | 설명 | |------|--------|------| | `PORT` | `8888` | Express 서버 포트 | | `QP_API_BASE` | `http://10.150.6.47:18503` | FastAPI 백엔드 URL | | `AUTH_API_BASE` | `http://10.150.6.47:8090` | LDAP 인증 서버 URL | | `SNAP_DATA_DIR` | `../snap_data` | 페이지 이미지 폴더 | | `NODE_ENV` | (없음) | `production`이면 dist/ 정적 서빙 | ### 작업 루트 seok@PSESL25717702:/mnt/d/dev/LLM/QP/service$ ls README.md assets index.html metadata.json node_modules package-lock.json package.json server.ts src tsconfig.json vite.config.ts ------------- # 배포 에러 ```bash seok@PSESL25717702:/mnt/d/dev/LLM/QP/service$ npm run build > react-example@0.0.0 build > vite build vite v6.4.2 building for production... ✓ 2072 modules transformed. dist/index.html 0.55 kB │ gzip: 0.40 kB dist/assets/favicon-CIqKAyFz.ico 1.62 kB dist/assets/index-BtKlGMUt.css 41.21 kB │ gzip: 7.45 kB dist/assets/index-D94KCa__.js 370.44 kB │ gzip: 114.85 kB ✓ built in 46.73s seok@PSESL25717702:/mnt/d/dev/LLM/QP/service$ npm start > react-example@0.0.0 start > node server.ts node:internal/modules/esm/get_format:219 throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath); ^ TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for D:\dev\LLM\QP\service\server.ts at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:219:9) at defaultGetFormat (node:internal/modules/esm/get_format:245:36) at defaultLoad (node:internal/modules/esm/load:120:22) at async ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:580:32) at async ModuleJob._link (node:internal/modules/esm/module_job:116:19) { code: 'ERR_UNKNOWN_FILE_EXTENSION' } Node.js v22.16.0 seok@PSESL25717702:/mnt/d/dev/LLM/QP/service$ ``` ## why?? ```bash h200_1_user@dgx-h200:/home/Projects/projects/qp/images$ docker ps -a --filter "name=snap" CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 045ccb3438cb snap:1.0 "docker-entrypoint.s…" 3 minutes ago Up 3 minutes (unhealthy) 0.0.0.0:18504->18505/tcp, [::]:18504->18505/tcp snap h200_1_user@dgx-h200:/home/Projects/projects/qp/images$ docker logs --tail 200 snap [snap_data] serving from /data/snap_data Server running on http://localhost:18505 [auth] Auth server: http://10.150.6.47:8090 [logs] Activity logs: /data/logs h200_1_user@dgx-h200:/home/Projects/projects/qp/images$ docker inspect snap | grep -A 30 "Health" "Health": { "Status": "unhealthy", "FailingStreak": 9, "Log": [ { "Start": "2026-05-07T17:57:37.139147043+09:00", "End": "2026-05-07T17:57:37.251677715+09:00", "ExitCode": 1, "Output": "wget: can't connect to remote host: Connection refused\n" }, { "Start": "2026-05-07T17:58:07.25268173+09:00", "End": "2026-05-07T17:58:07.298976582+09:00", "ExitCode": 1, "Output": "wget: can't connect to remote host: Connection refused\n" }, { "Start": "2026-05-07T17:58:37.299534102+09:00", "End": "2026-05-07T17:58:37.344486575+09:00", "ExitCode": 1, "Output": "wget: can't connect to remote host: Connection refused\n" }, { "Start": "2026-05-07T17:59:07.345563587+09:00", "End": "2026-05-07T17:59:07.390899772+09:00", "ExitCode": 1, "Output": "wget: can't connect to remote host: Connection refused\n" }, { "Start": "2026-05-07T17:59:37.392160333+09:00", "End": "2026-05-07T17:59:37.43758568+09:00", -- "Healthcheck": { "Test": [ "CMD-SHELL", "wget -qO- http://localhost:18505/ > /dev/null || exit 1" ], "Interval": 30000000000, "Timeout": 5000000000, "StartPeriod": 15000000000, "Retries": 3 }, "Image": "snap:1.0", "Volumes": null, "WorkingDir": "/app", "Entrypoint": [ "docker-entrypoint.sh" ], "Labels": {} }, "NetworkSettings": { "SandboxID": "074deb5cc38a95e97f0cd8eb91353d72f0db1fd10fdd602673c55e6423e86abc", "SandboxKey": "/var/run/docker/netns/074deb5cc38a", "Ports": { "18505/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "18504" }, { "HostIp": "::", "HostPort": "18504" } ``` --- ``` # ───────────────────────────────────────────── # SNAP Frontend + BFF (React + Express) # Multi-stage: Vite 빌드 → 경량 런타임 # # 변경 이력: # - LDAP 인증 통합 (cookie-parser, /api/auth/*) # - 활동 로그 시스템 (/data/logs 마운트 권장) # ───────────────────────────────────────────── # ─── Build stage ─── FROM node:20-alpine AS builder WORKDIR /app # 의존성 먼저 설치 (도커 레이어 캐싱) COPY package.json package-lock.json ./ RUN npm ci # 소스 전체 복사 → Vite 빌드 # (App.tsx, main.tsx, index.css, types.ts, searchPipeline.ts 모두 필요) COPY . . RUN npm run build # → /app/dist 에 React 빌드 결과 (HTML/JS/CSS 번들) # ─── Runtime stage ─── FROM node:20-alpine WORKDIR /app # 런타임 의존성만 설치 (devDependencies 제외) COPY package.json package-lock.json ./ RUN npm ci --omit=dev && \ npm install --no-save tsx@^4.21.0 && \ npm cache clean --force # Vite 빌드 결과 (HTML/JS/CSS 번들) COPY --from=builder /app/dist ./dist # BFF 서버 코드 + 런타임 import 대상 # server.ts가 src/services/searchPipeline.ts, src/types.ts를 import 함 COPY server.ts ./ COPY src ./src COPY tsconfig.json ./ # 활동 로그 디렉토리 생성 (호스트에서 -v로 마운트할 위치) # 마운트 안 하면 컨테이너 내부 임시 폴더에 기록됨 (재시작 시 소실) RUN mkdir -p /data/logs && chmod 755 /data/logs # ─── 환경변수 (런타임에 docker run --env-file로 주입) ─── ENV NODE_ENV=production ENV PORT=18505 EXPOSE 18505 # Healthcheck — 30초마다 / 응답 확인 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD wget -qO- http://localhost:18505/ > /dev/null || exit 1 CMD ["npx", "tsx", "server.ts"] ```