Files
egbim_qa_platform/FEEDBACK_SYNC_GUIDE.md
T

15 KiB

피드백 작성 서버 ↔ 관리 콘솔 동기화 운영 가이드

이 문서는 이 저장소의 보안형 피드백 작성 페이지를 https://feedback.hmac.kr/의 기존 관리 콘솔과 연결하고, Gitea Actions로 스테이징에 배포하는 방법을 설명합니다.

1. 먼저 이해할 구조

이 연동은 별도 배치 작업이나 DB 복제가 아닙니다. 사용자의 작성 요청마다 작성 서버가 관리 콘솔의 Support API를 호출하는 서버사이드 proxy 방식입니다.

브라우저
  │ same-origin 요청
  ▼
작성 서버의 Next.js API
  │ Authorization 전달 + multipart 중계
  ▼
https://feedback.hmac.kr/api/support
  │
  ├─ Secretary: 관리 티켓·권한·첨부파일 메타데이터
  ├─ ABC: 피드백 원본
  └─ 관리 콘솔: 목록·상세·상태·댓글·내부 메모

따라서 작성 서버에는 운영용 ABC API, Secretary API, MySQL, OpenSearch를 함께 배포하지 않습니다. 피드백 저장의 최종 확인 지점은 feedback.hmac.kr입니다.

2. 연동 전 준비

2.1 관리 콘솔 API 계약

관리 콘솔은 다음 경로를 제공해야 합니다.

GET  /api/support/access
GET  /api/support/workspaces/{workspaceCode}/form-template
POST /api/support/workspaces/{workspaceCode}/tickets
POST /api/support/tickets/{ticketId}/completion-confirmation?workspaceCode={workspaceCode}

작성 서버가 브라우저에 제공하는 내부 경로는 다음과 같습니다.

GET  /api/support/access
GET  /api/support/workspaces/{workspaceCode}/form-template
POST /api/support/workspaces/{workspaceCode}/submit
POST /api/support/tickets/{ticketId}/completion-confirmation?workspaceCode={workspaceCode}

submit은 multipart 요청을 파싱한 뒤 외부의 /tickets로 다시 전송합니다. 외부 API의 URL은 SUPPORT_CONSOLE_API_BASE_URL에서만 읽습니다.

완료 확인 요청은 작성자가 처리 결과를 확인했을 때 호출합니다. 요청 본문은 {}이며 작성 서버 proxy가 현재 로그인 세션의 Bearer 토큰과 workspaceCode를 관리 콘솔로 전달합니다. 관리 콘솔은 토큰의 user_id, tenant_id와 Q&A 작성자를 비교하고, 진행중·완료 안내 댓글·연결 이슈 완료·보류 여부를 검증해야 합니다. 같은 요청은 중복 상태 변경 없이 멱등하게 처리해야 합니다.

성공 응답은 다음 필드를 포함합니다.

{
  "feedback_id": 47,
  "feedback_status": "COMPLETED",
  "completed_at": "2026-09-04T12:00:00.000Z"
}

409는 진행중 상태가 아니거나 완료 안내 댓글이 없거나 연결 이슈가 아직 완료되지 않은 경우입니다. 기존 상태값을 사용하는 환경은 COMPLETED 대신 RESOLVED를 반환할 수 있으며, 작성페이지는 두 값을 완료로 표시합니다.

2.2 BARON-SSO RP 등록

작성 서버 전용 confidential client를 BARON-SSO에 등록합니다. 관리 콘솔의 client secret을 재사용하지 않습니다.

현재 스테이징 기본 주소를 직접 사용하는 경우 redirect URI는 다음과 같습니다.

http://10.13.10.4:8864/api/auth/baron-sso/callback

실제 공개 주소가 HTTPS 도메인이라면 그 주소로 바꾸고, scheme·host·port·path를 한 글자도 다르게 등록하지 않습니다. 코드가 요청의 HostX-Forwarded-Proto를 이용해 callback URI를 만들므로, 앞단 reverse proxy가 원래 scheme을 전달해야 합니다.

10.13.10.4:8864는 내부 스테이징 주소로 취급합니다. 인터넷에 직접 노출하지 말고 외부 공개가 필요하면 TLS reverse proxy를 앞에 두어 HTTPS, Host, X-Forwarded-Proto를 일관되게 전달합니다.

현재 production Compose와 workflow에는 SSO_REDIRECT_URI를 전달하지 않습니다. 고정 callback URI를 사용해야 하는 경우 BARON-SSO만 수정하지 말고 docker-compose.prod.yml, workflow, local-sso.ts의 동작을 함께 변경한 뒤 검증합니다.

2.3 JWT 계약

작성 서버는 BARON-SSO userinfo를 바탕으로 HS256 세션 JWT를 만들고 HttpOnly 쿠키에 넣습니다. proxy는 이 JWT를 Authorization: Bearer ...로 관리 콘솔에 전달합니다.

다음 두 조건이 모두 맞아야 합니다.

  • 작성 서버의 JWT_SECRET이 설정되어 있어야 합니다.
  • feedback.hmac.kr의 Support API가 같은 JWT_SECRET으로 토큰을 검증해야 합니다.

두 서버의 값이 다르면 SSO callback까지 성공해도 /api/support/access에서 401이 발생합니다. secret은 문서·이슈·로그에 기록하지 않고 보안 채널로 교환합니다.

2.4 workspace와 프로젝트 매핑

작성 URL의 {workspaceCode}는 관리 콘솔의 workspace 식별자입니다. 첫 예시는 다음입니다.

/support/EGBIM_DEMO/new

이전 연동 확인에서 EGBIM_DEMO는 관리 콘솔의 ABC projectId=8, channelId=9에 매핑되었습니다. 이 값은 환경별 데이터에 속하므로 배포 때 숫자를 코드에 고정하지 말고, 관리 콘솔에서 현재 mapping을 다시 확인합니다. 프로젝트나 채널 이름이 중복되거나 workspace가 비활성 상태면 양식 조회와 등록이 실패할 수 있습니다.

3. 작성 서버 환경값

운영에서 중요한 값은 다음과 같습니다.

NEXT_PUBLIC_API_BASE_URL=
NEXT_PUBLIC_FEEDBACK_ONLY=true
SUPPORT_CONSOLE_API_BASE_URL=https://feedback.hmac.kr/api/support
SSO_ISSUER=https://sso.hmac.kr/oidc
SSO_AUTHORIZATION_ENDPOINT=https://sso.hmac.kr/oidc/oauth2/auth
SSO_TOKEN_ENDPOINT=https://sso.hmac.kr/oidc/oauth2/token
SSO_USERINFO_ENDPOINT=https://sso.hmac.kr/oidc/userinfo
SSO_SCOPE=openid profile email
SSO_CLIENT_ID=<BARON-SSO 작성 서버 client id>
SSO_CLIENT_SECRET=<BARON-SSO 작성 서버 client secret>
JWT_SECRET=<관리 콘솔 Support API와 공유하는 HS256 secret>
SUPPORT_TENANT_ID=<userinfo에 tenant_id가 없을 때만>
WEB_PORT=8864

NEXT_PUBLIC_ 접두사가 붙은 값은 브라우저 빌드에 포함될 수 있습니다. SSO_CLIENT_SECRET, JWT_SECRET은 절대 NEXT_PUBLIC_로 시작하게 만들지 않습니다. 운영에서 외부 API URL을 브라우저용 NEXT_PUBLIC_API_BASE_URL에 넣으면 CORS와 secret 노출 경계가 깨집니다.

4. Gitea Actions 배포

등록해야 하는 키와 Variables/Secrets 구분은 GITEA_VARIABLES.md에 있습니다. workflow는 다음 순서로 실행됩니다.

  1. main push 또는 수동 실행으로 시작합니다.
  2. STAGING_HOST, WEB_PORT가 현재 고정된 스테이징 대상과 맞는지 검사합니다.
  3. SSH private key와 known_hosts를 runner 임시 디렉터리에 만들고 strict host key 검증으로 접속합니다.
  4. .git, node_modules, .next, dist, uploads, .venv 등을 제외한 소스를 원격 서버로 전송합니다.
  5. 환경값을 base64로 인코딩해 원격 Bash에 전달하고, 서버 디스크에 .env 파일로 저장하지 않습니다.
  6. docker compose ... config --quietup -d --build를 실행합니다.
  7. Web의 /api/health를 최대 60회 확인하고 실패하면 Web 상태와 최근 로그를 출력합니다.

원격 사용자의 login shell이 zsh이어도 동작하도록 마지막 배포 스크립트는 명시적으로 bash -s로 실행합니다. 이 방식은 이전에 대형 명령 문자열을 SSH로 전달할 때 발생한 중첩 따옴표·shell 문법 오류를 피하기 위한 것입니다.

5. 최초 배포 절차

5.1 코드와 설정 검사

git status --short
pnpm lint
pnpm typecheck
pnpm build

.env, .env.* 실제 환경파일, API key, secret, private key가 변경 목록에 없는지 확인합니다. apps/web/.env.example에는 빈 값 또는 설명용 placeholder만 둡니다.

5.2 Gitea 등록

Gitea 저장소의 Actions 변수/Secret 화면에서 이름을 정확히 입력합니다. 따옴표를 값에 포함하지 않습니다. multiline private key와 known_hosts는 줄바꿈을 보존합니다.

최소 등록 세트:

Variables: STAGING_HOST, WEB_PORT, SUPPORT_CONSOLE_API_BASE_URL,
           SSO_ISSUER, SSO_AUTHORIZATION_ENDPOINT, SSO_TOKEN_ENDPOINT,
           SSO_USERINFO_ENDPOINT, SSO_SCOPE, SSO_CLIENT_ID
Secrets:   STAGING_USER, STAGING_SSH_PRIVATE_KEY, STAGING_SSH_KNOWN_HOSTS,
           SSO_CLIENT_SECRET, JWT_SECRET

값의 기본값, 선택 여부, 등록 화면은 GITEA_VARIABLES.md를 기준으로 합니다.

5.3 BARON-SSO redirect와 네트워크 확인

  • Gitea runner에서 10.13.10.4:22로 SSH 접속할 수 있어야 합니다.
  • 스테이징 서버의 배포 사용자가 Docker 명령을 실행할 수 있어야 합니다.
  • 스테이징 서버에서 feedback.hmac.krsso.hmac.kr로 HTTPS 요청을 보낼 수 있어야 합니다.
  • BARON-SSO에 실제 접속 주소의 callback URI가 등록되어 있어야 합니다.

5.4 배포 실행

main에 push하거나 Gitea Actions에서 workflow_dispatch를 실행합니다. workflow가 성공해도 기능 검증은 별도로 진행합니다.

수동으로 원격에서 실행할 때는 secret을 shell history나 로그에 남기지 말고, 동일한 runtime 환경을 주입합니다.

docker compose -f docker/docker-compose.prod.yml config --quiet
docker compose -f docker/docker-compose.prod.yml up -d --build
docker compose -f docker/docker-compose.prod.yml ps

6. 배포 후 검증

  1. http://10.13.10.4:8864/api/health{ "status": "ok" }를 반환하는지 확인합니다.
  2. /support/EGBIM_DEMO/new 접속 시 로그인 화면으로 이동하는지 확인합니다.
  3. 로그인 후 양식이 관리 콘솔의 workspace template에서 표시되는지 확인합니다.
  4. 작성페이지에서 제목·구분·내용만 입력할 수 있는지 확인합니다. IP·MAC 필드는 작성자 화면에 표시하지 않습니다.
  5. 이미지뿐 아니라 PDF·문서 등 일반 파일도 첨부합니다.
  6. 성공 응답의 ticket_id, sync_status를 확인합니다.
  7. feedback.hmac.kr에서 같은 제목·작성자·첨부파일을 조회합니다.
  8. 관리 콘솔에서 처리 완료 안내 댓글을 등록한 뒤 작성자 상세페이지에 완료 확인 버튼과 요청 시각이 표시되는지 확인합니다.
  9. 완료 확인 버튼을 한 번 눌러 COMPLETED(또는 호환 환경의 RESOLVED)와 completed_at 응답을 확인합니다.
  10. 같은 버튼 요청을 반복해도 중복 완료 처리 없이 성공 응답이 유지되는지 확인합니다.
  11. 파일당 30MB 초과, 전체 30MB 초과, 10개 초과가 413으로 차단되는지 확인합니다.

관리 콘솔에 표시되지 않으면 작성 서버에서 데이터를 재생성하거나 DB를 초기화하지 않습니다. 브라우저 응답, 관리 콘솔 API 로그, workspace mapping, 두 서버의 JWT secret을 순서대로 확인합니다.

7. 시행착오에서 정리한 원칙

localhost API 주소를 production build에 넣지 않기

처음에는 브라우저용 NEXT_PUBLIC_API_BASE_URL과 서버 내부 주소가 섞여 CORS 또는 잘못된 대상 호출이 발생할 수 있었습니다. 현재 피드백 전용 빌드는 브라우저 API base URL을 빈 값으로 고정하고, 외부 주소는 서버 전용 변수로만 전달합니다.

관리 콘솔 login을 재사용하지 않기

작성 페이지 주소가 달라졌다고 관리 콘솔의 브라우저 세션을 그대로 사용하면 callback과 cookie 범위가 꼬입니다. 작성 서버가 별도 BARON-SSO RP로 code 교환을 수행하고, 관리 콘솔 API에는 세션 JWT만 전달합니다.

API key를 작성 브라우저에 넣지 않기

피드백 전용 작성 흐름은 사용자의 SSO 세션으로 관리 콘솔 API를 호출합니다. MASTER_API_KEY, SECRETARY_ABC_API_KEY, ABC API key를 브라우저 bundle이나 운영 Compose에 추가하지 않습니다. API key가 필요한 별도 서버 연동은 관리 콘솔 서버의 책임입니다.

첨부파일은 JSON이 아닌 multipart로 전달하기

첨부파일이 이미지에만 한정되거나 JSON으로 변환되면 PDF·문서 등록이 깨질 수 있습니다. 작성 서버의 proxy는 bodyParser: false와 formidable을 사용해 multipart를 읽고, 외부 API에 FormData로 전달합니다. 파일 제한은 클라이언트와 서버 양쪽에서 확인합니다.

원격 shell의 quoting에 의존하지 않기

SSH 한 줄 명령 안에 secret, 배열, loop를 모두 넣으면 login shell에 따라 문법이 달라질 수 있습니다. 현재 workflow는 SSH 전송 후 명시적인 bash -s 블록에서 환경을 복원하고 Compose를 실행합니다. 배포 workflow를 수정할 때 이 경계를 유지합니다.

작업 이력에서 확인할 수 있는 전환점

다음 commit들이 현재 운영 방식을 결정한 주요 기록입니다.

commit 의미
33453ec 최초 스테이징 배포 구성
aaddfc7 피드백 작성 API와 서버사이드 SSO/proxy 적용
483f5a0 첨부파일 전달 경로 수정
090e9fd 원격 login shell 차이를 피하기 위해 Bash script 실행 방식 수정

이전 workflow는 API·Secretary·DB 환경값을 대량으로 전송하고 ABC API key를 요구했지만, 현재 피드백 전용 workflow는 외부 관리 콘솔의 API 계약만 사용합니다. 과거 문서의 172.16.10.175:3030, NEXT_PUBLIC_API_BASE_URL 외부 주소, MASTER_API_KEY 필수 표기는 현재 배포 기준으로 사용하지 않습니다.

8. 장애 대응과 롤백

docker compose -f docker/docker-compose.prod.yml ps
docker compose -f docker/docker-compose.prod.yml logs --tail=200 web
curl -i http://127.0.0.1:8864/api/health
  • 401: SSO userinfo의 sub/tenant_id, JWT_SECRET 일치 여부, 관리 콘솔 access API를 확인합니다.
  • 404: 외부 base URL에 /api/support가 포함되는지와 workspace code를 확인합니다.
  • 413: 파일 개수·용량 제한을 확인합니다.
  • 502: 작성 서버에서 관리 콘솔로의 네트워크, DNS, TLS, 외부 API 상태를 확인합니다.
  • callback 오류: BARON-SSO redirect URI와 Host/X-Forwarded-Proto를 확인합니다.

롤백은 마지막 정상 commit을 기준으로 다시 배포합니다. 작성 서버에는 운영 DB가 없으므로 DB rollback을 수행하지 않지만, 관리 콘솔 DB migration이 포함된 버전과 이 서버의 피드백 전용 배포를 혼동하지 않습니다. 기존 데이터가 있는 어떤 서버에서도 docker compose down -v를 무심코 실행하지 않습니다.

9. 변경 시 함께 확인할 파일

  • docker/docker-compose.prod.yml: production이 web만 실행하는지
  • .gitea/workflows/deploy-staging.yml: 실제로 읽는 Variables/Secrets와 기본값
  • docker/web.dockerfile: build-time 공개 변수와 runtime server 변수의 경계
  • apps/web/src/server/local-sso.ts: callback, state, 세션 JWT, cookie
  • apps/web/src/server/support-external.ts: 외부 Support API URL 조립
  • apps/web/src/pages/api/support/workspaces/[workspaceCode]/submit.ts: multipart 중계와 제한
  • FEEDBACK_ONLY_TASKS.md: 작업 당시의 완료/보류 기록