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 λͺ
λ Ή
npm install cookie-parser
npm install -D @types/cookie-parser
git add server.ts src/App.tsx src/types.ts package.json package-lock.json
git commit -m "feat: LDAP μΈμ¦ ν΅ν© β λ‘κ·ΈμΈ κ²μ΄ν
+ HttpOnly μΏ ν€ + UserMenu"
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
λ°°ν¬ μλ¬
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??
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"]