Add the top-right account icon with the signed-in name and logout link. Expire the Q&A session before redirecting to the configured inquiry homepage in both environments. Preserve login return URL checks and ignore request-controlled logout destinations. Validation: 106 tests, syntax checks, and mocked Chromium desktop/mobile regressions pass, including session-cookie removal and the destination navigation request with external networking blocked.
EG-BIM Q&A static pages
egbim/index.html의 목록 위에 작성·상세 모달을 여는 EG-BIM Q&A UI입니다. SW문의_소스패키지의 히어로·제품 탭·목록·모달 디자인을 적용하고, 기존 첨부·댓글·비밀글·작성자 권한·검색을 유지합니다. 회사 홈페이지의 헤더·메뉴·푸터는 포함하지 않습니다. 기존 write.html, detail.html?id=... 주소는 같은 목록의 모달로 연결됩니다. 상단 오른쪽 사람 아이콘의 계정 메뉴에서 로그인 사용자 이름을 확인하고 로그아웃할 수 있습니다.
EG-BIM 탭만 가운데 표시하며 TOVA·GAIA는 오픈 전까지 숨깁니다. 원격지원은 https://939.co.kr/saman/으로 열립니다. UI 적용 범위·검증·남은 배포 확인은 작업 목록에 정리했습니다.
로그인부터 글 저장까지의 화면·처리 전이맵은 docs/qa-login-to-save-transition.md에 정리했습니다.
로컬 확인
정적 파일 서버에서 루트를 열면 됩니다.
python3 -m http.server 4173
그 다음 http://localhost:4173/egbim/에 접속합니다. 정적 서버는 화면 배치 확인용이며 로그인과 저장 API를 제공하지 않습니다. 브라우저 보안 정책 때문에 file:// 직접 열기보다 정적 서버를 사용하는 편이 안전합니다. 실제 서버 데이터를 변경하지 않는 모달 회귀 검증은 별도 Playwright 환경에서 npm run test:ui로 실행할 수 있습니다.
feedback.hmac.kr DB 기준 연동
덤프의 EG-BIM 운영 대상은 다음 값으로 고정했습니다.
workspaces.id = 6,workspace_code = EGBIMchannels.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 v7 방식으로 feedbackId를 생성하고 같은 Worker의 POST /api/feedbacks에 전달합니다. API 서버의 저장 구조와 작성자·조직 매핑은 이번 UI 변경 범위에 포함하지 않습니다. 기존 저장 대상은 다음과 같습니다.
feedbacks:id,channel_id,source_namespace,source_record_id,datasupport_tickets: 작성자/테넌트/제목/내용/분류/상태/idempotency_keyfeedback_comment_attachments: 답변 댓글 이미지가 생길 때comment_id와qa_cdn메타데이터support_attachments: 운영 호환 레이어가 필요할 때 동일 파일의 티켓 첨부 메타데이터
상세 페이지에는 공개 댓글 입력 UI가 있으며, 댓글은 같은 Worker를 통해 ABC의 feedback_comments에 저장하고 is_internal=false인 공개 댓글만 조회합니다. 댓글 이미지가 있으면 attachments multipart 필드로 Worker가 ABC에 전달하며, 응답으로 받은 이미지 URL을 썸네일로 표시합니다. 목록·상세 진입 시에는 ABC의 feedback_status를 다시 조회해 관리페이지에서 변경된 상태를 반영합니다.
게시글과 댓글의 수정·삭제 버튼은 로그인 사용자의 기본 requester UUID와 작성자 ID가 일치할 때만 표시합니다. 이름·이메일이 같다는 이유로 표시하지 않으며, 작성자 ID가 없거나 로그인 확인 중·만료·조회 실패 상태이면 숨깁니다. 다른 사람의 게시글에서도 본인이 작성한 댓글의 버튼은 표시합니다. 공통 버튼 CSS는 hidden 속성을 덮어쓰지 않습니다.
목록 검색은 제목·내용·회사·부서·작성자를 대상으로 오타, 띄어쓰기·대소문자 차이, 두 글자 이상의 한글 초성을 처리합니다. 예를 들어 로그임, ㄹㄱㅇ으로 로그인을, 파일첨부로 파일 첨부를 찾을 수 있습니다. 검색 버튼이나 Enter로 실행하며, 여러 단어를 입력하면 각 단어가 모두 일치해야 합니다. 정확한 제목 일치를 우선하고 같은 점수는 기존 목록 순서를 유지합니다. 분류와 내가 작성한 글 조건도 함께 적용합니다.
너무 짧은 검색어의 과도한 일치를 줄이기 위해 12글자는 정확한 포함 검색을 사용합니다. 오타는 35글자에 1회, 6~32글자에 최대 2회의 삽입·삭제·치환·인접 문자 교환을 허용합니다. 긴 단어는 정확한 포함 검색을 사용합니다. 목록은 API가 반환하는 페이지를 순서대로 수집하므로 첫 100건 이후의 글도 검색합니다. 로딩 중과 일부 페이지 조회 실패는 화면에 표시하며, 조회하지 못한 글까지 검색했다고 안내하지 않습니다.
연동 지점
- 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 세션 또는 토큰을 서버 측에서 검증해야 합니다. - 작성자 식별자: Worker가 검증한 세션의
requesterId와requesterTenantId를 사용합니다. 브라우저는 회사·부서·이름·이메일을 읽기 전용으로 표시하고, 필수 UUID가 없으면 제출을 차단합니다. 서버가 요청의 작성자·조직 정보를 결정합니다. - API: 작성페이지는 같은 Worker의
POST /api/feedbacks를 호출합니다. Worker가 Q&A 세션 쿠키를 검증하고, SSO requester 정보와ABC_API_KEYSecret을 추가한 뒤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/에 유지합니다.
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을 만들고 값을 채운 뒤 등록합니다.
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은 정규화된 작성자 정보만 반환합니다.
로그아웃 메뉴는 /auth/logout으로 이동해 해당 환경의 Q&A 세션 쿠키를 삭제한 뒤 https://test.baroncs.co.kr/ko/sw-inquiry.html로 돌아갑니다. 스테이징·운영의 AUTH_POST_LOGOUT_REDIRECT_URI에 같은 목적지를 설정합니다. 목적지는 서버 배포 설정에서만 읽으며 요청 쿼리로 변경할 수 없습니다. 중앙 SSO의 로그인 종료 요청은 별도로 보내지 않습니다.
Gitea Actions 등록값
운영 qa.baroncs.co.kr 배포는 production 파이프라인 안내를 따른다. 운영 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 값은 로그에 출력하지 않습니다.
화면에는 디자인 목업의 예시 글·공지를 넣지 않습니다. 브라우저는 실제 Worker API를 호출하며 기존 localStorage는 조회·저장 결과의 캐시로 유지합니다. API 설정 전 로컬 저장 성공으로 대체하지 않습니다. 운영은 production 파이프라인 안내의 PROD_ 등록값과 기존 수동 배포 절차를 사용합니다.
회사·부서 조직 연동 (스테이징)
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만으로 조직 조회 성공을 판단하지 않습니다.
조직 API는 Workers가 지원하는 redirect: manual로 호출하고, 3xx 응답은 실패 처리해 다른 주소로 인증 헤더를 전달하지 않습니다. Workers에서는 redirect: error가 요청 전 TypeError를 발생시킵니다. npm test에는 설치된 workerd로 실제 /auth/session 경로를 실행하는 검사가 포함됩니다. 외부 API는 로컬 모의 서비스로 대체하며 조직 매핑과 리다이렉트 거부를 함께 확인합니다.
로컬 검증:
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 번들에 넣지 않습니다.