133 lines
11 KiB
Markdown
133 lines
11 KiB
Markdown
# EG-BIM Q&A static pages
|
|
|
|
`egbim/` 아래의 `index.html`, `write.html`, `detail.html` 세 페이지로 구성한 EG-BIM Q&A UI입니다. 원본 `egbim_homepage`의 Q&A 화면 구성과 패키지 S/W 메뉴 방향을 참고해, PHP/그누보드/DB 의존성 없이 정적 호스팅에서 동작하도록 분리했습니다.
|
|
|
|
로그인부터 글 저장까지의 화면·처리 전이맵은 [`docs/qa-login-to-save-transition.md`](docs/qa-login-to-save-transition.md)에 정리했습니다.
|
|
|
|
## 로컬 확인
|
|
|
|
정적 파일 서버에서 루트를 열면 됩니다.
|
|
|
|
```bash
|
|
python3 -m http.server 4173
|
|
```
|
|
|
|
그 다음 `http://localhost:4173/egbim/`에 접속합니다. 브라우저 보안 정책 때문에 `file://` 직접 열기보다 정적 서버를 사용하는 편이 안전합니다.
|
|
|
|
## feedback.hmac.kr DB 기준 연동
|
|
|
|
덤프의 EG-BIM 운영 대상은 다음 값으로 고정했습니다.
|
|
|
|
- `workspaces.id = 6`, `workspace_code = EGBIM`
|
|
- `channels.name = Q&A`, `channels.id = 01a0ae40-51c2-7647-a93c-0249b3759777`
|
|
- 카테고리: `ERROR_QNA`, `IMPROVEMENT_QNA`, `GENERAL_QNA`
|
|
- 신규 상태: `support_tickets.status_code = RECEIVED`, `feedback_status = NEW`
|
|
- 첨부 저장소: `storage_bucket = qa_cdn`
|
|
|
|
작성 시 브라우저가 UUID를 하나 생성합니다. 같은 UUID를 `feedback.id`, `feedback.source_record_id`, `support_tickets.idempotency_key`에 사용하므로 중복 제출을 판별할 수 있습니다. API 서버는 envelope를 받아 다음 DB 레코드를 하나의 트랜잭션으로 생성해야 합니다.
|
|
|
|
- `feedbacks`: `id`, `channel_id`, `source_namespace`, `source_record_id`, `data`
|
|
- `support_tickets`: 작성자/테넌트/제목/내용/분류/상태/`idempotency_key`
|
|
- `feedback_comment_attachments`: 답변 댓글 이미지가 생길 때 `comment_id`와 `qa_cdn` 메타데이터
|
|
- `support_attachments`: 운영 호환 레이어가 필요할 때 동일 파일의 티켓 첨부 메타데이터
|
|
|
|
상세 페이지에는 공개 댓글 입력 UI가 있으며, 댓글은 같은 Worker를 통해 ABC의 `feedback_comments`에 저장하고 `is_internal=false`인 공개 댓글만 조회합니다. 댓글 이미지가 있으면 `attachments` multipart 필드로 Worker가 ABC에 전달하며, 응답으로 받은 이미지 URL을 썸네일로 표시합니다. 목록·상세 진입 시에는 ABC의 `feedback_status`를 다시 조회해 관리페이지에서 변경된 상태를 반영합니다.
|
|
|
|
## 연동 지점
|
|
|
|
- SSO: Q&A는 홈페이지 로그인 정보를 전달받지 않고, `https://sso.hmac.kr/oidc`에 등록된 독립적인 PKCE Public Client로 직접 인증합니다. callback은 `https://qa-test.baroncs.co.kr/auth/callback`이며, Worker가 검증한 claim을 버전이 지정된 HttpOnly 세션 쿠키에 저장합니다. 브라우저의 `GET /auth/session`은 Q&A 세션에 저장된 작성자 정보를 반환합니다. feedback 서버는 반드시 SSO 세션 또는 토큰을 서버 측에서 검증해야 합니다.
|
|
- 작성자 식별자: `ssoSubject`/`requesterId`, `userUuid`, `tenantId`/`requesterTenantId`, `tenantIds`, `scope`, `roles`, 이메일·이름·부서·전화번호를 payload에 넣습니다. `requester_id`와 `requester_tenant_id`가 없으면 제출을 차단합니다.
|
|
- API: 작성페이지는 같은 Worker의 `POST /api/feedbacks`를 호출합니다. Worker가 Q&A 세션 쿠키를 검증하고, SSO requester 정보와 `ABC_API_KEY` Secret을 추가한 뒤 `POST https://feedback.hmac.kr/api/projects/{projectId}/channels/{channelId}/feedbacks`로 전달합니다. API Key와 내부 ABC 주소는 브라우저에 노출하지 않습니다.
|
|
- 첨부파일: 작성페이지가 `multipart/form-data`의 반복 `images` 필드로 파일을 Worker에 전달하고, Worker가 검증된 requester 정보와 API Key를 추가해 ABC API로 전달합니다. ABC API가 `qa_cdn` 저장소에 파일을 저장하므로 R2 access key/secret은 정적 페이지에 넣지 않습니다.
|
|
- 텍스트만 등록할 때는 기존 JSON 요청을 사용하고, 파일이 있을 때만 multipart 요청을 사용합니다.
|
|
|
|
## Cloudflare R2
|
|
|
|
`wrangler.toml`과 `src/index.js`를 추가해 Worker가 R2 정적 파일을 제공하도록 구성했습니다. 루트(`/`)와 `/index.html`은 `/egbim/`으로 이동하고, `/egbim/` 아래의 EG-BIM Q&A 페이지는 로그인 보호 경로입니다. EG-BIM 전용 페이지 파일은 `egbim/`에 두고, 스타일과 폰트는 여러 S/W가 공유할 수 있도록 `assets/`에 유지합니다.
|
|
|
|
```bash
|
|
npm install
|
|
npm run check
|
|
npm run r2:upload
|
|
npm run deploy
|
|
```
|
|
|
|
`wrangler deploy`는 Worker와 R2 binding을 배포하고, `npm run r2:upload`는 `egbim/` 페이지와 공통 `assets/`를 R2 bucket에 올립니다. `npx wrangler r2 object put`은 각 파일을 개별 업로드하므로 업로드 결과를 확인하기 쉽습니다.
|
|
|
|
## Wrangler secret 등록
|
|
|
|
실제 secret은 저장소에 만들지 않습니다. `qa-secrets.json.example`을 복사해 `qa-secrets.json`을 만들고 값을 채운 뒤 등록합니다.
|
|
|
|
```bash
|
|
cp qa-secrets.json.example qa-secrets.json
|
|
openssl rand -hex 32
|
|
npx wrangler secret bulk qa-secrets.json --name baron-qa-gateway-test
|
|
```
|
|
|
|
`ABC_API_KEY`에는 프로젝트 전용 ABC API Key를 등록합니다. API Key는 작성페이지 코드나 `egbim/config.js`에 넣지 않습니다.
|
|
|
|
`AUTH_CLIENT_ID`, `AUTH_AUTHORIZE_URL`, `AUTH_TOKEN_URL`, 선택적인 `AUTH_USERINFO_URL`은 `wrangler.toml`에 실제 SSO 값으로 설정해야 합니다. 현재 Q&A RP는 `https://sso.hmac.kr/oidc`를 사용합니다. 이 RP는 PKCE Public Client이므로 `AUTH_CLIENT_SECRET`은 사용하지 않으며, `SESSION_SECRET`만 secret으로 등록합니다.
|
|
|
|
Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증한 사용자 claim을 서명된 HttpOnly 세션 쿠키에 저장합니다. 브라우저의 `GET /auth/session`은 정규화된 작성자 정보만 반환합니다.
|
|
|
|
## Gitea Actions 등록값
|
|
|
|
운영 `qa.baroncs.co.kr` 배포는 [production 파이프라인 안내](docs/qa-production-pipeline.md)를 따른다. 운영 workflow는 `Deploy EG-BIM QA Production`이며 `main`에서 수동으로 실행한다. 아래 기존 등록값은 스테이징 배포용이다.
|
|
|
|
저장소 Settings → Actions → Secrets에 아래 기본 Secret 3개를 등록합니다.
|
|
|
|
| 이름 | 종류 | 값 |
|
|
|---|---|---|
|
|
| `CLOUDFLARE_API_TOKEN` | Secret | Workers Scripts Edit + Workers R2 Storage Edit 권한의 Cloudflare API Token |
|
|
| `SESSION_SECRET` | Secret | `openssl rand -hex 32`로 생성한 세션 서명키 |
|
|
| `ABC_API_KEY` | Secret | 프로젝트 전용 ABC UserFeedback API Key |
|
|
|
|
`CLOUDFLARE_ACCOUNT_ID`는 secret으로 등록할 필요가 없습니다. `wrangler.toml`에 `81fa2d48964d31dd0da9558f9ce601d1`로 설정되어 있습니다.
|
|
|
|
Cloudflare API Token에는 최소한 다음 권한이 필요합니다.
|
|
|
|
- Account → Workers Scripts → Edit
|
|
- Account → Workers R2 Storage → Edit
|
|
- Account → Account Settings → Read
|
|
- Custom Domain route를 Actions에서 변경할 경우 Zone → Workers Routes → Edit
|
|
|
|
`.gitea/workflows/deploy.yml`은 `main` push 또는 수동 실행 시 세션 secret을 Worker에 등록하고 R2 업로드 후 `baron-qa-gateway-test`를 배포합니다. 실제 secret 값은 로그에 출력하지 않습니다.
|
|
|
|
현재 화면은 API 설정 전에도 QA 흐름을 확인할 수 있도록 샘플 글과 로컬 테스트 저장을 포함합니다. 운영 배포 시에는 Gitea Secret `ABC_API_KEY`가 Worker에 등록되어야 하며, 작성페이지는 Worker 프록시를 통해 ABC API를 호출합니다.
|
|
|
|
|
|
## 회사·부서 조직 연동 (스테이징)
|
|
|
|
Gitea의 스테이징 설정을 `.gitea/workflows/deploy.yml`에서 Worker 런타임 이름으로 전달합니다.
|
|
|
|
| Gitea | 위치 | Worker 런타임 |
|
|
|---|---|---|
|
|
| `STAGING_ORG_CONTEXT_KEY_ID` | Secrets | `ORG_CONTEXT_KEY_ID` (Secret) |
|
|
| `STAGING_ORG_CONTEXT_KEY_SECRET` | Secrets | `ORG_CONTEXT_KEY_SECRET` (Secret) |
|
|
| `STAGING_ORG_CONTEXT_ENABLED` | Variables | `ORG_CONTEXT_ENABLED` |
|
|
| `STAGING_ORG_CONTEXT_API_URL` | Variables | `ORG_CONTEXT_API_URL` |
|
|
| `STAGING_ORG_CONTEXT_SCOPE_MODE` | Variables | `ORG_CONTEXT_SCOPE_MODE` |
|
|
| `STAGING_ORG_CONTEXT_EXPOSURE` | Variables | `ORG_CONTEXT_EXPOSURE` |
|
|
| `STAGING_ORG_CONTEXT_TENANT_IDS` | Variables | `ORG_CONTEXT_TENANT_IDS` |
|
|
| `STAGING_ORG_CONTEXT_CACHE_TTL_SECONDS` | Variables, 선택 | `ORG_CONTEXT_CACHE_TTL_SECONDS` (기본 300) |
|
|
|
|
조회는 `SCOPED`, `identifiers`를 사용합니다. 기준 테넌트는 UUID JSON 배열로 등록하며 UUID v4를 허용합니다. 각 테넌트를 최대 3개씩 동시 조회하고, 완전한 응답만 캐시합니다. 페이지 조회 타임아웃은 요청당 5초입니다. 배포 전 `npm run org:verify`는 요청당 15초를 허용하고, 타임아웃·통신 실패·HTTP 429/502/503/504에 한해 2초 간격으로 최대 3회 시도합니다. 인증 실패·범위 불일치·구성원 누락은 재시도 없이 검증 실패로 처리합니다.
|
|
|
|
기본 `ORG_CONTEXT_ENABLED=false`에서는 조직 API를 호출하지 않습니다. `true`로 배포하면 `npm run org:verify`가 실제 키로 조직 API를 먼저 검증하며, 실패 시 새 Worker 배포를 진행하지 않습니다. 성공 시에는 조직·사용자·소속 건수만 로그에 출력합니다. 전체 응답·전화번호·키 원문은 출력하지 않습니다.
|
|
|
|
Worker는 서명된 세션의 사용자 UUID로 소속을 찾습니다. 조회 조직 컨텍스트·대표 소속을 선택하고, 기존 requester ID·tenant ID·권한은 유지합니다. 새 문의는 `requester_affiliation`에 회사, `requester_department`에 부서를 저장합니다. JSON과 첨부 multipart에 같은 값을 사용합니다. 목록·상세는 글에 저장된 정보를 사용하며 조회 시점의 조직으로 과거 글을 바꾸지 않습니다.
|
|
|
|
조직 조회가 실패하면 기존 세션·글쓰기 흐름과 확인된 SSO 정보를 유지합니다. 누락되거나 모호한 사용자는 `orgContextStatus`로 구분하고 다른 작성자의 정보를 대신 쓰지 않습니다. 로그인 후 `/auth/session`의 `user.orgContextStatus=unavailable`이면 `user.orgContextError`에서 허용된 오류 코드만 확인할 수 있습니다. `org_context_missing_credentials`는 Worker의 조직 API 키 누락, `org_context_http_401`은 API 인증 거부, `org_context_http_403`은 API 접근 거부, `org_context_timeout`은 요청 시간 초과를 뜻합니다. 알 수 없는 오류는 `org_context_unavailable`로 표시하며 키·원본 오류 메시지·조직 디렉터리는 응답에 넣지 않습니다. 조회가 정상화되면 이전 `orgContextError`는 제거됩니다. HTTP 200이나 요청 로그의 `outcome=ok`만으로 조직 조회 성공을 판단하지 않습니다.
|
|
|
|
로컬 검증:
|
|
|
|
```sh
|
|
npm run check
|
|
npm test
|
|
WRANGLER_LOG_PATH=/tmp/qa-worker-build.log node scripts/deploy-worker.mjs --dry-run
|
|
```
|
|
|
|
마지막 명령은 Secret 등록이나 배포를 실행하지 않고 번들만 확인합니다. 실제 키를 로컬로 준비했다면 런타임 환경변수 이름으로 주입한 뒤 `npm run org:verify`를 실행할 수 있습니다. 키 값을 커맨드라인 인자나 소스에 넣지 않습니다.
|
|
|
|
기존 마이그레이션 글의 보정은 `baron_qa/scripts/qa_org_metadata.py` 도구에서 별도로 수행합니다. 개인정보가 포함된 엑셀·보정 보고서를 이 저장소의 정적 파일이나 Worker 번들에 넣지 않습니다.
|