# 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 번들에 넣지 않습니다.