Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
사내 AI 챗 포털 - nginx auth_request 게이트웨이 + FastAPI 앱 9개 + OpenAI Tool Calling. 실제 운영본을 가짜 데이터로 바꿔 공개, 체험 계정 포함
Translation pending
BusinessPython · HTMLGitHub @9seebirdOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗체험판 https://portal.trendkit.app/ (demo / demo1234) 왜 이렇게 만들었는지는 케이스 스터디에 있습니다. 이 문서는 무엇이 들어 있고, 어떻게 띄우고, 체험판에서 무엇이 막혀 있는지를 다룹니다.
직원이 주소 하나로 들어와서, 한국어로 물어보면 사내 앱이 대신 일해 주는 포털입니다. 로그인·권한·이력·AI 비용 관리가 한곳에 있고, 그 뒤에 업무 앱 아홉 개가 붙어 있습니다.
실제로 회사에서 쓰던 것을 가짜 데이터로 바꿔 공개한 것입니다. 아래 계정으로 들어가면 전부 눌러 보실 수 있습니다.
체험 계정
demo/demo1234보기 · 엑셀 올려 계산 · 챗 · 검색은 전부 됩니다. 계정 · 자산 · 서비스처럼 다음 사람에게 남는 것만 저장되지 않습니다.
IT 담당자 한 명이 회사의 전산을 다 봅니다. 그러다 보면 이렇게 됩니다.
그래서 로그인·권한·이력·AI 비용을 포털 하나가 맡고, 앱은 자기 일만 하게 나눴습니다. 앱을 새로 붙일 때 만드는 것은 그 앱의 계산 로직뿐입니다.
| 앱 | 하는 일 | 눈여겨볼 곳 |
|---|---|---|
| 포털 | 로그인 · 권한 · 챗 · 이력 · AI 사용량 | 접근 범위(부서/개인/제외) · 사용량 대시보드 |
| IT 자산 관리 | 자산 · 직원 · IP · 내선 · 자리 · 라이선스 · 렌탈 | 퇴사자 미반납 · 라이선스 초과 배정 잡아내기 |
| 사업계획 예상손익 | 엑셀 둘 → 채널·브랜드·월별 손익표 | 틀렸을 때 숫자를 안 내보내는 것 |
| 연차 이상감지 | 고립 연차 · 저사용 · 몰아쓰기 | 요일 가중치 · 집단연차 제외 |
| AI 이용 현황 | 게이트웨이 원본 로그 → 대시보드 | 브라우저 안에서 집계 (서버 부담 0) |
| 매뉴얼 가져오기 | PPT → 웹 매뉴얼 자동 변환 | 표를 절차로 펴기 · 작은 그림 버리기 |
| IT 안내 | 매뉴얼 · 운영 체크리스트 · 비용 · 프롬프트 생성기 · 사내앱 규칙 · AI 참고자료 · 자가진단 | 정적 페이지 7장 · 챗이 그 안을 검색 |
| 온라인 교육 이수 | 지정 영상 시청 진도 → 자동 이수 · 이수율 | 스크롤바로 건너뛰면 이수 안 됨 |
| 앱 입고 | 올린 앱 검사 → AI 가 고치기 → 검토 요청 → 담당자 승인·반려 | 걸린 것을 AI 가 고치고 다시 검사까지 가는 것 · 담당자에게 「무엇이 바뀌었나」를 세 겹으로 보여 주는 것 |
브라우저
│
▼
┌─────────────┐ ① 이 사람이 이걸 써도 되나?
│ nginx │ ─────────────────────────┐
│ (입구 하나) │ ▼
└─────────────┘ ┌───────────┐
│ ② 되면 그때만 넘긴다 │ 포털 │
│ (X-User-Id 를 붙여서) │ 로그인 │
├──────────────┐ │ 권한 │
▼ ▼ │ 챗 │
┌────────┐ ┌──────────┐ └───────────┘
│ 자산 │ │ 사업계획 │ … │
└────────┘ └──────────┘ │ ③ 챗은 앱의 API 에
│ 물어보고 답만 옮긴다
▼
같은 숫자
앱은 자기 로그인을 만들지 않습니다. nginx 가 요청마다 포털에 물어보고
(auth_request), 통과하면 그때만 앱으로 넘기면서 누구인지를 헤더에 붙여 줍니다.
앱이 보는 것은 X-User-Id 네 줄뿐입니다.
챗은 계산하지 않습니다. "우리 팀 자산 현황 알려줘" 를 받으면 자산 앱의 API 를 부르고 그 답을 옮깁니다. 포털이 따로 계산하면 화면과 챗이 다른 숫자를 말하게 됩니다.
같은 구조로 붙어 있는 앱 하나를 예로 보면 이렇습니다 — 직원은 자기 진도만, 담당자는 전사 이수율을 봅니다. 같은 앱, 같은 데이터, 권한만 다릅니다.
포트폴리오에서 제일 보실 만한 부분이라고 생각합니다. "무엇을 만들었나" 보다 "왜 그렇게 만들었나" 쪽입니다.
챗 안에서 엑셀을 읽어 답하면 빠르게 만들 수 있습니다. 대신 같은 계산이 두 벌이 되고, 어느 날 화면과 챗이 다른 숫자를 말합니다. 그때 어느 쪽이 맞는지 알 방법이 없습니다. 그래서 챗은 앱의 API 만 부릅니다. 느려지는 대신 숫자가 하나입니다.
JWT 안의 값만 믿으면 관리자 권한을 회수해도 토큰이 만료되는 12시간 동안 예전 권한이 그대로 삽니다. 화면에서 메뉴가 안 사라지는 정도가 아니라 관리자 API 가 계속 열려 있다는 뜻입니다. 그래서 요청마다 DB 를 한 번 봅니다. SQLite 조회 하나라 이 규모에서는 체감되지 않습니다.
처음에는 {'H&B': '헬스뷰티'} 같은 표가 코드에 박혀 있었습니다.
채널이 늘 때마다 코드를 고쳐 다시 배포해야 하고, 무엇보다
코드의 이름과 파일의 이름이 어긋나면 그 채널 매출이 조용히 빠집니다.
지금은 마스터 파일 한 곳에서만 관리합니다.
「파일에 적힌 합계」와 「계산 결과」를 맞춰 보면 대부분의 사고가 잡힙니다. 그런데 열이 한 칸 밀린 파일에서는 둘 다 똑같이 틀린 채로 일치합니다. 같은 칸에서 읽으니까요. 실제로 41억이 사라졌는데 「일치합니다」가 떴습니다. 그래서 월 이름이 몇 번째 칸에 있는지를 따로 확인합니다.
전 직원 교육을 무료 강의로 돌리면 콘텐츠는 아쉽지 않습니다. 잃는 것은 수강 기록 하나입니다. 그래서 그 기록만 회사가 갖게 만들었습니다.
처음에는 최대 재생 위치를 저장했습니다. 그러면 스크롤바를 맨 뒤로 한 번 끄는 것으로 이수가 끝납니다. 그래서 영상을 10초짜리 칸으로 쪼개고, 화면이 5초마다 보내오는 위치로 지나간 칸만 채웁니다.
영상은 이 앱이 내보내지 않습니다. 유튜브가 재생하고 앱은 위치만 받아 적어서, 몇 명이 붙어도 서버 부하가 사실상 없습니다. 자체 서빙을 택하지 않은 이유는 uvicorn 워커가 스트림을 붙들고 있는 동안 그 앱의 다른 요청이 밀리기 때문입니다.
이수 기준은 90%입니다. 100%로 두면 엔딩 카드까지 보는 사람이 드물어서 다 본 사람이 미이수로 남습니다.
담당자가 차시의 영상 주소를 다른 것으로 고칠 수 있습니다. 그때 진도를 그대로 두면 옛 영상에서 본 15분이 새 영상의 15분으로 둔갑합니다 — 안 본 강의가 이수로 잡히는 셈입니다. 그래서 영상이 바뀌면 그 차시의 진도만 초기화하고, 제목·안내문·기준만 고칠 때는 건드리지 않습니다.
교육 앱은 포털 없이 혼자 돌 수도 있습니다(AUTH_MODE). 포털이 아직 전 직원에게
열리지 않은 상태에서 교육을 먼저 돌려야 할 때를 위해서입니다.
이때 무엇을 사람의 열쇠로 쓸지가 갈림길이었습니다. 직원은 사번을 더 잘 알지만, 사번으로 기록을 쌓아 두면 나중에 포털에 붙일 때 그동안의 이수 기록이 끊깁니다(포털이 넘겨주는 것은 계정 아이디이므로). 그래서 저장은 아이디로 하고, 로그인할 때만 사번으로도 찾아 주게 했습니다. 두 방식이 같은 모양의 사용자 정보를 만들어 내므로 나머지 코드는 어느 쪽으로 들어왔는지 알지 못합니다.
앱이 뜰 때 포털에 "나 여기 있다"고 알립니다. 이름·설명·검색어를 고치면 저절로 반영됩니다. 다만 중지 상태로 들어옵니다. 누가 볼지 정하고 켜는 것은 사람이 합니다. 앱이 스스로 켜지면 아무나 볼 수 있게 되니까요.
컨테이너 이름이 portal · proxy · it-asset 이었습니다. 사내에서 혼자
쓸 때는 문제가 없지만, 공개 저장소는 이미 뭔가 띄워 둔 PC 로 갑니다.
이름이 겹치는 것은 오류가 나니 그나마 낫습니다. 진짜 문제는 이쪽이었습니다 —
docker compose 는 이름을 안 적으면 폴더 이름으로 프로젝트를 정합니다.
받는 쪽에도 it-asset 폴더가 있으면, compose 가 남의 컨테이너를 이
프로젝트 것으로 보고 docker compose exec 를 그 안에서 돌립니다.
실제로 그렇게 됐습니다. 체험판 자료를 넣는 명령이 남의 앱 안에서 실행됐고, 그 앱에는 그 파일이 없어서 실패로 끝났습니다. 있었으면 남의 데이터가 덮였을 겁니다.
세 가지를 한꺼번에 고쳤습니다.
demo- 로 (demo-portal · demo-proxy …)name: demo-<앱> 을 못 박아 폴더 이름에 기대지 않게docker compose exec 대신 docker exec <이름> —
이름 하나만 보므로 헷갈릴 여지가 없습니다네트워크도 portal-net → demo-net 으로 바꿨습니다.
앱마다 compose 를 따로 둡니다. 하나로 묶으면 앱 하나 고칠 때마다 전부 재시작합니다.
대신 모두 같은 도커 네트워크에 붙어 컨테이너 이름으로 서로를 부릅니다.
deploy.sh 가 순서를 압니다 — 포털이 먼저(앱이 등록해야 하니까), 프록시가 마지막(502 방지).
공개 데모는 아무나 눌러 보는 곳입니다. 로그인을 열어 둔 이상 이런 일이 실제로 일어납니다.
「전부 읽기 전용」으로 만들면 셋 다 막히지만 볼 게 없어집니다. 그래서 기준을 하나로 잡았습니다 — 남는 것만 막는다.
| 막지 않는다 | 올리기 · 계산 · 챗 · 검색 · 내려받기 (요청이 끝나면 사라지는 것) |
| 막는다 | 계정 · 자산 · 서비스 · 부서 · 비밀번호 (다음 사람에게 남는 것) |
예외가 하나 있습니다 — 자리 배치도에서 칸 옮기기는 열어 두었습니다. 남기는 하지만 화면에 되돌리기 버튼이 있고, 틀어져도 배치 자료를 다시 넣으면 원래대로 돌아옵니다. 이 앱에서 제일 보여줄 만한 화면이 배치도인데 막아 두면 눌러 볼 것이 없어서, 되돌릴 수 있는가에 무게를 두었습니다. 칸 만들기 · 지우기 · 사람 앉히기는 되돌리기로도 안 돌아와서 그대로 막습니다.
막는 자리는 두 겹입니다. 서로 못 보는 곳이 다르기 때문입니다.
portal/app/demo.py — 포털 자기 주소를 본다. 미들웨어라 새 주소도 자동으로 검사를 지난다proxy/conf.d/00-demo-guard.conf — 앱 아홉 개를 전부 본다.
바꾸는 요청은 전부 막고 안전한 것만 이름을 적어 연다. 새 앱을 붙이면 막힌 채로 시작한다챗은 세 겹으로 묶습니다 — IP 당 하루 20번 · 하루 전체 15만 토큰 · 이번 달 전체 300만 토큰.
가운데가 있는 이유는 IP 는 바꾸면 그만이기 때문입니다. 달 한도만 두면 한 사람이 하루 만에 다 태울 수 있고, 그러면 남은 한 달 내내 챗이 죽어 있습니다. 하루 한도가 있으면 최악의 날도 다음 날 아침에 되살아납니다. 어느 한도에 걸리든 챗만 쉬고 나머지 앱은 그대로 열려 있습니다.
앱 입고만 예외입니다. 이 포털에서 유일하게 「남이 준 파일을 서버에서 푸는」 앱이라, 공개된 곳에 그 길을 열어 두지 않았습니다. 체험판에서는 미리 넣어 둔 예시 셋(규칙대로 만든 앱 · 로그인을 스스로 만든 앱 · 여기저기 틀린 앱)만 검사합니다. 검사하는 코드는 올릴 때와 같은 것을 씁니다 — 예시만 특별 대접을 받으면 「예시에서는 되는데」가 되기 때문입니다.
진짜로 막혀 있는지는 설정을 읽는 게 아니라 바깥에서 요청을 보내 확인합니다.
python3 tools/check_demo.py https://내주소
파이썬이 없으면(윈도우가 대개 그렇습니다) 컨테이너로 돌려도 같습니다. 도커 네트워크 안에서 프록시를 이름으로 부르므로 주소를 몰라도 됩니다.
docker run --rm --network demo-net -v "%CD%\tools:/t" python:3.12-slim python /t/check_demo.py http://demo-proxy
■ 막혀야 한다 — 다음 사람에게 남는 것
통과 POST /api/me/password 403 ← 이게 뚫리면 데모가 끝난다
통과 POST /it-asset/api/assets 403
통과 POST /manual-import/api/apply 403
…
■ 열려야 한다 — 계산하고 버리는 것
통과 POST /biz-plan/api/run 200
통과 POST /manual-import/api/preview 200
한 번은 이 검사를 통과하는데도 화면에는 안내가 안 뜬 적이 있습니다. nginx 가 내보내는 403 본문의 줄바꿈이 JSON 으로 안 읽히는 상태였습니다. 상태 코드는 멀쩡해서 눈치채기 어려웠습니다. 지금은 본문이 JSON 으로 읽히는지도 함께 봅니다.
도커만 있으면 됩니다. 파이썬도 따로 필요 없습니다 — 데이터를 만드는 일까지 컨테이너 안에서 합니다.
윈도우와 리눅스가 같은 순서, 같은 이름입니다.
| 윈도우 (PowerShell) | 리눅스 | |
|---|---|---|
| 처음 준비 | .\setup.ps1 | ./setup.sh |
| 띄우기 | .\deploy.ps1 | ./deploy.sh |
| 하나만 | .\deploy.ps1 portal | ./deploy.sh portal |
| 체험판 전체 | .\demo.ps1 | ./demo.sh |
| 서버(nginx 뒤) | .\demo.ps1 -Behind | ./demo.sh --behind |
| 포트 바꾸기 | .\demo.ps1 -Port 9000 | DEMO_PORT=9000 ./demo.sh |
띄우면 http://localhost:8090/ 입니다. 80 이 아닙니다 —
받는 사람의 PC 에는 이미 뭔가 80 을 쓰고 있을 가능성이 높아서,
체험판이 남의 자리를 뺏지 않게 했습니다.
git clone <이 저장소>
cd portal-portfolio
# AI 챗까지 보려면 키를 넣습니다. 없어도 나머지 앱은 전부 돌아갑니다.
copy portal\.env.example portal\.env
notepad portal\.env # OPENAI_API_KEY
.\demo.ps1 # 이 한 줄이면 끝납니다
이 시스템에서 스크립트를 실행할 수 없으므로오류가 나면 한 번만:Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
git clone <이 저장소>
cd portal-portfolio
cp portal/.env.example portal/.env
$EDITOR portal/.env # OPENAI_API_KEY
./demo.sh
둘 다 하는 일이 같습니다:
setup.sh — .env 들을 만들고 앱끼리 쓰는 토큰을 서로 맞춥니다
(토큰이 한 글자만 달라도 앱이 포털을 못 부르는데 화면에는 단서가 없습니다)DEMO_MODE=1 · 계정 · 한도)tools/make_demo_data.py — 가짜 회사 데이터를 만듭니다deploy.sh — 순서대로 띄웁니다tools/check_demo.py — 안전장치가 진짜로 막는지 확인합니다두 번 실행해도 안전합니다. 이미 있는 것은 건드리지 않습니다.
앞에 nginx 를 두고 HTTPS 를 붙이는 경우:
./demo.sh --behind # 127.0.0.1:8081 로만 열립니다
자세한 절차(도커 설치 · 방화벽 · 인증서 · 백업)는 docs/DEPLOY.md 에 있습니다.
전부 가상 회사 「미르」 의 지어낸 값입니다.
사람·부서·채널을 tools/demo_company.py 한 곳에만 적어 두고 나머지가 읽어 갑니다.
그래야 챗에서 "한윤현 자산 뭐 써?" 를 물었을 때
포털 계정 · 자산관리 · 휴가가 같은 사람을 가리킵니다.
숫자만 채우면 표는 차지만 볼 것이 없습니다. 각 앱이 찾아내라고 만든 것을 일부러 심어 두었습니다.
자산 퇴사자 2명이 노트북·라이선스 미반납 · 라이선스 4개 사서 6명 씀(-2)
라이선스 2종 곧 만료 · 렌탈 3건 두 달 안에 종료
연차 위험 3 · 검토 6 · 관찰 6 · 저사용 5 · 급증 3 (50명 중)
사업계획 채널 이름 어긋남 · 열 밀림을 넣으면 계산을 멈춤
넣는 방식은 각 앱의 파서를 거칩니다. DB 에 직접 쓰면 「업로드로는 통과 못 할 자료」가 화면에 앉게 되고, 파서가 망가져도 데모는 멀쩡해 보입니다. 지금은 자료가 보인다는 것 자체가 그 앱이 돈다는 확인입니다.
python3 tools/make_demo_data.py --force # 다시 만들기
파이썬이 없으면 tools/Dockerfile 로 만든 작은 이미지를 씁니다
(demo.sh · demo.ps1 이 알아서 고릅니다).
docker build -t portfolio-tools tools
docker run --rm -v "%CD%:/w" -w /w portfolio-tools python tools/make_demo_data.py --force
.env — 열쇠는 저장소에 없습니다. .env.example 만 있습니다원 저작자·회사 확인을 받고 공개한 것입니다.
MIT. LICENSE 참고.