diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 4f104e0..aa9304e 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -12,6 +12,7 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + ABC_API_KEY: ${{ secrets.ABC_API_KEY }} steps: - name: Checkout @@ -28,6 +29,7 @@ jobs: set -euo pipefail test -n "$CLOUDFLARE_API_TOKEN" || { echo "CLOUDFLARE_API_TOKEN is missing"; exit 1; } test -n "$SESSION_SECRET" || { echo "SESSION_SECRET is missing"; exit 1; } + test -n "$ABC_API_KEY" || { echo "ABC_API_KEY is missing"; exit 1; } - name: Install dependencies run: npm install --no-fund --no-audit @@ -41,6 +43,12 @@ jobs: set -euo pipefail printf '%s' "$SESSION_SECRET" | npx wrangler secret put SESSION_SECRET --name baron-qa-gateway-test + - name: Register ABC API secret + shell: bash + run: | + set -euo pipefail + printf '%s' "$ABC_API_KEY" | npx wrangler secret put ABC_API_KEY --name baron-qa-gateway-test + - name: Upload static files to R2 run: npm run r2:upload diff --git a/README.md b/README.md index 2530958..262959e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ `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)에 정리했습니다. + ## 로컬 확인 정적 파일 서버에서 루트를 열면 됩니다. @@ -33,9 +35,9 @@ python3 -m http.server 4173 ## 연동 지점 -- SSO: 헤더 로그인 링크와 작성 페이지의 로그인 가이드는 `https://test.baroncs.co.kr/`로 연결됩니다. `baron_user`, `baron_claims`, Descope 쿠키, JWT payload와 `sessionStorage`를 우선 읽고, `ssoSessionEndpoint`가 설정되면 `credentials: include`로 세션 bridge를 호출합니다. 브라우저에서 읽은 JWT claim은 표시/전송용 힌트일 뿐이며, feedback 서버는 반드시 SSO 세션 또는 토큰을 서버 측에서 검증해야 합니다. +- SSO: Q&A는 홈페이지 로그인 정보를 전달받지 않고, `https://app.brsw.kr/oidc`에 독립적인 PKCE Public Client로 직접 인증합니다. callback은 `https://qa-test.baroncs.co.kr/auth/callback`이며, Worker가 검증한 claim을 `baron_qa_session` HttpOnly 쿠키에 저장합니다. 브라우저의 `GET /auth/session`은 Q&A 세션에 저장된 작성자 정보를 반환합니다. feedback 서버는 반드시 SSO 세션 또는 토큰을 서버 측에서 검증해야 합니다. - 작성자 식별자: `ssoSubject`/`requesterId`, `userUuid`, `tenantId`/`requesterTenantId`, `tenantIds`, `scope`, `roles`, 이메일·이름·부서·전화번호를 payload에 넣습니다. `requester_id`와 `requester_tenant_id`가 없으면 제출을 차단합니다. -- API: `assets/config.js`의 `apiBaseUrl`에 API origin을 넣으면 `POST {apiBaseUrl}/v1/qa/uploads/presign`으로 업로드 URL을 받고, 파일을 `qa_cdn`에 직접 업로드한 뒤 `POST {apiBaseUrl}/v1/qa/feedbacks`로 DB용 envelope를 보냅니다. 두 엔드포인트의 인증/응답 규격은 실제 feedback 서버에 맞춰야 합니다. +- API: 작성페이지는 같은 Worker의 `POST /api/feedbacks`를 호출합니다. Worker가 `baron_qa_session`을 검증하고, SSO requester 정보와 `ABC_API_KEY` Secret을 추가한 뒤 `POST https://feedback.hmac.kr/api/projects/{projectId}/channels/{channelId}/feedbacks`로 전달합니다. API Key와 내부 ABC 주소는 브라우저에 노출하지 않습니다. - presign 응답: `{ "uploads": [{ "uploadUrl": "...", "storageKey": "...", "storageBucket": "qa_cdn", "headers": {} }] }` 형태를 기대합니다. R2 access key/secret은 정적 페이지에 넣지 않습니다. - API 주소가 비어 있으면 테스트를 위해 브라우저 `localStorage`에만 저장하며, 첨부파일은 `local-preview/...` 메타데이터만 생성합니다. @@ -62,6 +64,8 @@ openssl rand -hex 32 npx wrangler secret bulk qa-secrets.json --name baron-qa-gateway-test ``` +`ABC_API_KEY`에는 프로젝트 전용 ABC API Key를 등록합니다. API Key는 작성페이지 코드나 `assets/config.js`에 넣지 않습니다. + `AUTH_CLIENT_ID`, `AUTH_AUTHORIZE_URL`, `AUTH_TOKEN_URL`, 선택적인 `AUTH_USERINFO_URL`은 `wrangler.toml`에 실제 SSO 값으로 설정해야 합니다. 현재 Q&A RP는 `https://app.brsw.kr/oidc`를 사용합니다. 이 RP는 PKCE Public Client이므로 `AUTH_CLIENT_SECRET`은 사용하지 않으며, `SESSION_SECRET`만 secret으로 등록합니다. Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증한 사용자 claim을 서명된 HttpOnly 세션 쿠키에 저장합니다. 브라우저의 `GET /auth/session`은 정규화된 작성자 정보만 반환합니다. @@ -74,6 +78,7 @@ Worker는 OAuth Authorization Code + PKCE를 사용하고, callback에서 검증 |---|---|---| | `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`로 설정되어 있습니다. @@ -86,4 +91,4 @@ Cloudflare API Token에는 최소한 다음 권한이 필요합니다. `.gitea/workflows/deploy.yml`은 `main` push 또는 수동 실행 시 세션 secret을 Worker에 등록하고 R2 업로드 후 `baron-qa-gateway-test`를 배포합니다. 실제 secret 값은 로그에 출력하지 않습니다. -현재 화면은 API 설정 전에도 QA 흐름을 확인할 수 있도록 샘플 글과 로컬 테스트 저장을 포함합니다. 운영 반영 시 `apiBaseUrl`과 feedback API endpoint를 설정하고 로컬 fallback 제거 여부를 결정하세요. +현재 화면은 API 설정 전에도 QA 흐름을 확인할 수 있도록 샘플 글과 로컬 테스트 저장을 포함합니다. 운영 배포 시에는 Gitea Secret `ABC_API_KEY`가 Worker에 등록되어야 하며, 작성페이지는 Worker 프록시를 통해 ABC API를 호출합니다. diff --git a/app.js b/app.js index 6f7b699..139ffcf 100644 --- a/app.js +++ b/app.js @@ -186,8 +186,10 @@ } async function requestJson(path, options) { - if (!config.apiBaseUrl) return { local: true }; - const response = await fetch(config.apiBaseUrl.replace(/\/$/, '') + path, Object.assign({ credentials: 'include' }, options)); + const sameOriginProxy = !config.apiBaseUrl && path === config.createFeedbackPath; + if (!config.apiBaseUrl && !sameOriginProxy) return { local: true }; + const target = config.apiBaseUrl ? config.apiBaseUrl.replace(/\/$/, '') + path : path; + const response = await fetch(target, Object.assign({ credentials: 'include' }, options)); if (!response.ok) { const errorBody = await response.json().catch(function () { return {}; }); throw new Error(errorBody.message || 'API 요청에 실패했습니다. (' + response.status + ')'); @@ -200,12 +202,20 @@ } function makeUuid() { - if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID(); - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) { - const random = Math.random() * 16 | 0; - const value = char === 'x' ? random : (random & 3 | 8); - return value.toString(16); - }); + const bytes = new Uint8Array(16); + if (window.crypto && typeof window.crypto.getRandomValues === 'function') window.crypto.getRandomValues(bytes); + else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.random() * 256 | 0; + const timestamp = Date.now(); + bytes[0] = Math.floor(timestamp / 0x10000000000) & 0xff; + bytes[1] = Math.floor(timestamp / 0x100000000) & 0xff; + bytes[2] = Math.floor(timestamp / 0x1000000) & 0xff; + bytes[3] = Math.floor(timestamp / 0x10000) & 0xff; + bytes[4] = Math.floor(timestamp / 0x100) & 0xff; + bytes[5] = timestamp & 0xff; + bytes[6] = (bytes[6] & 0x0f) | 0x70; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.prototype.map.call(bytes, function (byte) { return ('0' + byte.toString(16)).slice(-2); }).join(''); + return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20); } async function sha256(file) { @@ -242,38 +252,12 @@ function buildFeedbackPayload(post, user, feedbackId, attachments) { const categoryCode = CATEGORY_CODES[post.category]; - const source = { namespace: config.sourceNamespace, recordId: feedbackId }; const imageMetadata = attachments.filter(function (item) { return item.purpose === 'FEEDBACK_ATTACHMENT'; }).map(function (item) { return { id: item.id, original_file_name: item.originalFileName, storage_bucket: item.storageBucket, storage_key: item.storageKey, mime_type: item.mimeType, file_size: item.fileSize, checksum_sha256: item.checksumSha256 }; }); - const data = { - id: feedbackId, - createdAt: post.createdAt, - title: post.title, - contents: post.content, - priority: 'MEDIUM', - Category: categoryCode, - requester_id: user.requesterId, - requester_tenant_id: user.requesterTenantId, - requester_email: user.email, - requester_name: user.name, - requester_department: user.department, - is_secret: post.secret ? 1 : 0, - feedback_status: 'NEW', - images: imageMetadata - }; - return { - schemaVersion: 'feedback.hmac.kr/qa.v1', - source: source, - feedback: { id: feedbackId, channelId: config.channelId, workspaceId: config.workspaceId, workspaceCode: config.workspaceCode, sourceNamespace: source.namespace, sourceRecordId: source.recordId, data: data }, - supportTicket: { - workspace_id: config.workspaceId, requester_id: user.requesterId, requester_tenant_id: user.requesterTenantId, ticket_type: 'QNA', source_system: 'EGBIM', title: post.title, category_code: categoryCode, status_code: 'RECEIVED', is_secret: post.secret ? 1 : 0, priority: 'NORMAL', description: post.content, approval_status: 'NOT_REQUIRED', sync_status: 'PENDING', issue_link_status: 'NOT_REQUIRED', feedback_status: 'NEW', idempotency_key: feedbackId, requester_email: user.email, requester_name: user.name, requester_department: user.department, requester_phone_number: user.phone, extra_fields: { feedback_id: feedbackId, channel_id: config.channelId } - }, - attachments: attachments.map(function (item) { return Object.assign({}, item, { sourceNamespace: source.namespace, sourceRecordId: source.recordId }); }), - comments: [], - commentAttachments: [], - author: { userUuid: user.userUuid, ssoSubject: user.ssoSubject, requesterId: user.requesterId, tenantId: user.tenantId, tenantIds: user.tenantIds, scope: user.scope, roles: user.roles, email: user.email, name: user.name, department: user.department, phone: user.phone } - }; + const payload = { feedbackId: feedbackId, title: post.title, contents: post.content, Category: categoryCode, is_secret: post.secret ? 1 : 0 }; + if (imageMetadata.length) payload.images = imageMetadata; + return payload; } function initList() { @@ -350,7 +334,8 @@ const attachments = await uploadFiles(qs('#attachment').files, feedbackId, 'FEEDBACK_ATTACHMENT'); const payload = buildFeedbackPayload(post, currentUser, feedbackId, attachments); const result = await postToApi(config.createFeedbackPath, payload); - if (result.local) savePost(post); + post.id = result.id || post.id; + savePost(post); showToast(result.local ? '테스트 글로 저장했습니다. UUID가 생성되었습니다.' : '문의가 등록되었습니다.'); window.setTimeout(function () { window.location.href = 'detail.html?id=' + encodeURIComponent(post.id); }, 500); } catch (error) { diff --git a/assets/config.js b/assets/config.js index 3b18233..a1ecb18 100644 --- a/assets/config.js +++ b/assets/config.js @@ -5,11 +5,12 @@ window.QA_CONFIG = Object.assign({ ssoUrl: '/auth/login', ssoSessionEndpoint: '/auth/session', presignPath: '/v1/qa/uploads/presign', - createFeedbackPath: '/v1/qa/feedbacks', + createFeedbackPath: '/api/feedbacks', + projectId: '01a0ae3f-fcf6-74b5-bdc4-70d942d6ad72', workspaceId: 6, workspaceCode: 'EGBIM', channelId: '01a0ae40-51c2-7647-a93c-0249b3759777', - sourceNamespace: 'baron_qa_write', + sourceNamespace: 'EGBIM_QA', storageBucket: 'qa_cdn', pageSize: 10 }, window.QA_CONFIG || {}); diff --git a/docs/qa-feedback-api-integration-task.md b/docs/qa-feedback-api-integration-task.md new file mode 100644 index 0000000..1ef450c --- /dev/null +++ b/docs/qa-feedback-api-integration-task.md @@ -0,0 +1,104 @@ +# EG-BIM Q&A 피드백 저장 API 연동 Task + +기준일: 2026-09-21 +대상: `qa-test.baroncs.co.kr` / Cloudflare Worker `baron-qa-gateway-test` + +## 목표 + +작성페이지의 문의를 Cloudflare Worker를 통해 ABC UserFeedback API에 저장한다. + +```text +작성페이지 + → Worker POST + → Q&A SSO 세션 확인 + → Worker가 requester 정보와 API Key 추가 + → ABC UserFeedback API + → feedbacks 저장 + → { id } 반환 + → 상세 페이지 이동 +``` + +## 확정 설정 + +| 항목 | 값 | +|---|---| +| Worker | `baron-qa-gateway-test` | +| 작성페이지 | `https://qa-test.baroncs.co.kr` | +| ABC API | `https://feedback.hmac.kr` | +| projectId | `01a0ae3f-fcf6-74b5-bdc4-70d942d6ad72` | +| channelId | `01a0ae40-51c2-7647-a93c-0249b3759777` | +| source namespace | `EGBIM_QA` | +| API Key Secret | `ABC_API_KEY` | + +## 1차 범위: 텍스트 문의 저장 + +- [x] 프로젝트·채널 UUID 확인 +- [x] 채널 필드 확인: `title`, `contents`, `Category`, `images` +- [x] SSO 세션 저장 및 `/auth/session` 확인 +- [x] Worker에 `POST /api/feedbacks` 라우트 추가 +- [x] Worker에서 `baron_qa_session` 검증 +- [x] Worker에서 requester 정보 추출 +- [x] `requester_id` 매핑: SSO `sub` +- [x] `requester_tenant_id` 매핑: SSO `tenant_id` +- [x] `requester_name` 매핑: SSO profile/name +- [x] `requester_email` 매핑: SSO profile/email +- [x] `requester_department` 매핑: SSO tenant/profile department +- [x] `requester_phone_number` 매핑: SSO `profile.phones[0]` 또는 phone claim +- [x] 브라우저 요청의 requester 값을 신뢰하지 않도록 처리 +- [x] Worker Secret `ABC_API_KEY`로 `x-api-key` 추가 +- [x] `projectId`와 `channelId`를 Worker 설정에 등록 +- [x] `POST /api/projects/{projectId}/channels/{channelId}/feedbacks` 호출 +- [x] `x-source-namespace` 추가 +- [x] `x-source-record-id` 추가 +- [x] `x-idempotency-consumer` 추가 +- [x] `Idempotency-Key`와 `idempotency-key` 호환 처리 +- [x] 응답에서 feedback ID를 추출해 `{ id }` 형식으로 반환 +- [x] API 실패 시 ABC 오류를 노출하지 않고 안전한 오류 응답 반환 +- [x] 성공 시 작성페이지에서 `detail.html?id={id}`로 이동 + +## 2차 범위: 첨부파일 + +- [ ] R2 업로드 결과의 `storageKey`를 ABC `images` 필드에 연결 +- [ ] ABC API가 허용하는 이미지 메타데이터 형식 확인 +- [ ] presigned URL 및 첨부파일 오류 처리 +- [ ] 이미지 포함 저장 테스트 + +## 보안 요구사항 + +- [x] API Key를 정적 JavaScript, HTML, `assets/config.js`에 넣지 않음 +- [x] API Key는 Cloudflare Worker Secret에만 저장 +- [x] requester 정보는 브라우저 입력값이 아닌 검증된 SSO 세션에서 생성 +- [x] 전화번호는 화면 입력값을 받지 않고 SSO 프로필에서만 읽음 +- [x] Worker 로그에 API Key, SSO token, 전화번호 원문을 기록하지 않음 +- [x] CORS는 동일 Worker 도메인 요청을 기준으로 제한 + +## 검증 시나리오 + +- [ ] 로그인하지 않은 사용자는 `401` 응답을 받음 +- [ ] 로그인한 사용자가 제목·내용·카테고리를 입력하면 ABC에 1건 저장됨 +- [ ] 저장된 데이터에 제목·내용·Category가 정확히 들어감 +- [ ] 저장된 데이터에 requester ID·tenant·이름·이메일·부서·전화번호가 들어감 +- [ ] 동일한 `Idempotency-Key` 재요청 시 중복 저장되지 않음 +- [ ] ABC 응답의 `id`로 상세 페이지 이동 +- [ ] API Key가 브라우저 Network 탭에 노출되지 않음 +- [ ] 잘못된 카테고리 또는 필드 입력은 ABC에 전달되기 전에 차단됨 + +## 배포 전 작업 + +- [ ] 실제 프로젝트 전용 ABC API Key 발급 +- [ ] Cloudflare Worker Secret 등록 + +```bash +npx wrangler secret put ABC_API_KEY --name baron-qa-gateway-test +``` + +- [ ] `qa-test.baroncs.co.kr`에서 로그인 후 텍스트 문의 1건 등록 +- [ ] Worker Logs에서 API Key·전화번호가 노출되지 않는지 확인 +- [ ] ABC 관리페이지에서 requester 전화번호를 포함한 저장 결과 확인 + +## 구현 메모 + +- ABC 문서에는 현재 `/api/projects/...` 경로가 실제 시나리오로 기재되어 있다. +- `/api/v1/...`는 패키징 권장 경로로 문서화되어 있으므로 1차 구현은 현재 운영 시나리오인 `/api/...`를 사용한다. +- UUID v7 정책에 맞춰 현재 브라우저의 UUID v4 생성 로직을 교체한다. +- `requester_phone_number`는 관리페이지 문서의 SSO 매핑 및 알림 정책에서 필수 requester 메타데이터로 정의되어 있다. ABC 채널의 동적 필드로 직접 저장 가능한지 여부는 API 응답에 맞춰 Worker에서 검증한다. diff --git a/docs/qa-login-to-save-transition.md b/docs/qa-login-to-save-transition.md new file mode 100644 index 0000000..398a118 --- /dev/null +++ b/docs/qa-login-to-save-transition.md @@ -0,0 +1,166 @@ +# EG-BIM Q&A 로그인부터 글 저장까지 전이맵 + +기준일: 2026-09-21 +대상: `qa-test.baroncs.co.kr` / EG-BIM Q&A +구성: 목록(`index.html`) · 작성(`write.html`) · 상세(`detail.html`) + +## 1. 전체 흐름 + +홈페이지 로그인 정보를 Q&A 사이트로 직접 전달하지 않는다. 홈페이지의 Q&A 링크는 별도 호스트로 이동하고, Q&A는 같은 BRSW OIDC SSO에 직접 인증을 요청한다. 이미 중앙 SSO 세션이 있으면 사용자는 로그인 화면을 보지 않고 Q&A 세션을 발급받는다. + +```mermaid +flowchart TD + A[바론 홈페이지
EG-BIM Q&A 링크] --> B[Q&A 목록 진입
https://qa-test.baroncs.co.kr/egbim/] + B --> C{Q&A 세션 쿠키
baron_qa_session 유효?} + + C -- 예 --> G[정적 목록 페이지 표시] + C -- 아니오 --> D[302 /auth/login
return_url 보관] + D --> E[Worker가 PKCE 생성
state · verifier · nonce] + E --> F[BRSW OIDC SSO
app.brsw.kr/oidc] + F --> F1{중앙 SSO 세션 존재?} + F1 -- 아니오 --> F2[SSO 로그인 화면] + F2 --> F3[최초 동의 화면
필요 시 1회] + F1 -- 예 --> H[승인 코드 발급] + F3 --> H + H --> I[/auth/callback?code&state] + I --> J[Worker가 state 검증
PKCE code_verifier 검증] + J --> K[Token Endpoint 호출
Public Client · client_secret 없음] + K --> L[UserInfo 조회 및 claim 정규화] + L --> M[baron_qa_session 발급
HttpOnly · Secure · SameSite=Lax] + M --> G + + G --> N[문의 등록 버튼] + N --> O[작성 페이지
write.html] + O --> P[GET /auth/session
작성자·tenant 표시] + P --> Q{userUuid와 tenantId 존재?} + Q -- 아니오 --> R[제출 차단
다시 로그인 안내] + Q -- 예 --> S[카테고리·제목·내용·비밀글 입력] + S --> T[등록 버튼 클릭] + T --> U[브라우저 UUID 생성
feedbackId] + U --> V{첨부파일 존재?} + V -- 아니오 --> Y[첨부 메타데이터 빈 배열] + V -- 예 --> W[POST presign 요청] + W --> X[qa_cdn presigned URL로
파일 PUT 업로드] + X --> Y[첨부파일 메타데이터 확정] + Y --> Z[feedback envelope 생성] + Z --> AA{apiBaseUrl 설정 여부} + AA -- 비어 있음
현재 테스트 모드 --> AB[localStorage에 임시 저장] + AA -- 설정됨
실서비스 모드 --> AC[POST /v1/qa/feedbacks
feedback.hmac.kr API] + AB --> AD[등록 완료 toast
상세 페이지 이동] + AC --> AE{API 성공?} + AE -- 예 --> AD + AE -- 아니오 --> AF[오류 표시
재시도 가능] +``` + +## 2. 상태 전이표 + +| 상태 | 화면/주소 | 진입 조건 | 주요 처리 | 다음 상태 | +|---|---|---|---|---| +| `S0` | 홈페이지 | Q&A 메뉴 클릭 | 외부 Q&A URL로 이동 | `S1` | +| `S1` | `/egbim/` | Q&A 세션 유효 | R2의 목록 HTML 제공 | `S5` | +| `S2` | `/auth/login` | 세션 없음 | PKCE 상태 생성, SSO로 redirect | `S3` | +| `S3` | `app.brsw.kr/oidc` | SSO 세션 없음 | 최초 로그인 및 동의 | `S4` | +| `S4` | `/auth/callback` | `code`, `state` 수신 | state/PKCE 검증, token/userinfo 조회 | `S5` | +| `S5` | 목록 또는 작성 페이지 | `baron_qa_session` 유효 | `/auth/session`에서 사용자 확인 | `S6` | +| `S6` | `write.html` | `userUuid`, `tenantId` 확인 | 작성 폼 입력 대기 | `S7` | +| `S7` | 작성 페이지 | 등록 클릭 | `feedbackId` UUID 생성 및 입력 검증 | `S8` | +| `S8` | 작성 페이지 | 첨부 있음 | presign → `qa_cdn` 업로드 | `S9` | +| `S9` | 작성 페이지 | payload 준비 | feedback/support ticket envelope 생성 | `S10` | +| `S10` | API 또는 localStorage | `apiBaseUrl` 상태에 따라 분기 | 실제 DB 저장 또는 테스트 저장 | `S11` | +| `S11` | `detail.html?id=...` | 저장 성공 | 등록 완료 및 상세 이동 | 종료 | + +## 3. 화면별 사용자 경험 + +### A. 홈페이지 + +- EG-BIM 메뉴의 Q&A 링크만 제공한다. +- 로그인 정보, 쿠키, token, `SESSION_SECRET`을 Q&A로 전달하지 않는다. +- 링크 대상: + +```text +https://qa-test.baroncs.co.kr/egbim/ +``` + +### B. 중앙 SSO + +- Q&A RP는 `https://app.brsw.kr/oidc`를 사용한다. +- 최초 사용자는 SSO 로그인 및 동의 화면을 볼 수 있다. +- 같은 중앙 SSO 세션이 있으면 다음 접근부터 로그인 화면 없이 authorization code가 발급된다. +- Q&A RP는 PKCE Public Client이므로 client secret을 보내지 않는다. + +### C. Q&A 목록 + +- Worker가 `baron_qa_session`을 검사한다. +- 세션이 없으면 `/auth/login?return_url=...`로 이동한다. +- 세션이 있으면 R2의 `egbim/index.html`을 제공한다. + +### D. Q&A 작성 + +- 브라우저가 `GET /auth/session`을 호출한다. +- 다음 작성자 정보를 화면과 payload에 사용한다. + +```text +userUuid +ssoSubject / requesterId +tenantId / requesterTenantId +tenantIds +scope +roles +email +name +department +phone +``` + +- `requesterId` 또는 `requesterTenantId`가 없으면 제출을 차단한다. + +### E. 글 저장 + +- 브라우저에서 `feedbackId` UUID를 1회 생성한다. +- 같은 UUID를 다음 식별자로 재사용한다. + +```text +feedback.id +feedback.source_record_id +support_tickets.idempotency_key +``` + +- 첨부파일은 `qa_cdn`에 저장하고, DB에는 bucket/key 및 파일 메타데이터를 기록한다. +- 실제 운영에서는 feedback API가 서버 측에서 Q&A 세션 또는 SSO token을 검증해야 한다. + +## 4. 현재 테스트 모드와 운영 모드 + +현재 `assets/config.js`는 다음과 같이 `apiBaseUrl`이 비어 있다. + +```js +apiBaseUrl: '' +``` + +따라서 현재 등록 결과는 다음과 같이 동작한다. + +```text +글 작성 → UUID 생성 → localStorage 저장 → 상세 페이지 이동 +``` + +운영 API 주소를 설정하면 다음 흐름으로 변경된다. + +```text +첨부 presign 요청 +→ qa_cdn 직접 업로드 +→ feedback.hmac.kr/v1/qa/feedbacks 호출 +→ feedbacks/support_tickets 트랜잭션 저장 +→ 상세 페이지 이동 +``` + +## 5. 주요 실패 전이 + +| 실패 지점 | 사용자 화면 | 원인/확인 항목 | +|---|---|---| +| SSO 로그인 전 | 로그인 화면 | 중앙 SSO 세션 없음 또는 다른 SSO 환경 | +| callback | `invalid_oauth_state` | OAuth cookie 만료, 호스트 변경, state 불일치 | +| token 교환 | `oauth_token_exchange_failed` | issuer/endpoint/client ID/PKCE 설정 불일치 | +| 세션 확인 | `authenticated:false` | `baron_qa_session` 없음·만료·`SESSION_SECRET` 불일치 | +| 작성자 확인 | UUID/tenant 오류 | SSO scope 또는 claim mapping 누락 | +| 첨부 업로드 | 업로드 실패 | presign API 또는 `qa_cdn` 권한 오류 | +| DB 저장 | API 요청 오류 | `apiBaseUrl`, CORS, API 인증, DB envelope 규격 오류 | + diff --git a/docs/관리페이지 md 파일/# EG-BIM Q&A 로그인부터 글 저장까지 전이맵.md b/docs/관리페이지 md 파일/# EG-BIM Q&A 로그인부터 글 저장까지 전이맵.md new file mode 100644 index 0000000..1ec73e5 --- /dev/null +++ b/docs/관리페이지 md 파일/# EG-BIM Q&A 로그인부터 글 저장까지 전이맵.md @@ -0,0 +1,461 @@ +# EG-BIM Q&A 로그인부터 글 저장까지 전이맵 + +기준일: 2026-09-21 +대상: `qa-test.baroncs.co.kr` / EG-BIM Q&A +구성: 목록(`index.html`) · 작성(`write.html`) · 상세(`detail.html`) · 관리페이지 · WORKS 알림 + +## 1. 전체 흐름 + +홈페이지 로그인 정보를 Q&A 사이트로 직접 전달하지 않는다. 홈페이지의 Q&A 링크는 별도 호스트로 이동하고, Q&A는 같은 BRSW OIDC SSO에 직접 인증을 요청한다. 이미 중앙 SSO 세션이 있으면 사용자는 로그인 화면을 보지 않고 Q&A 세션을 발급받는다. + +```mermaid +flowchart TD + A[바론 홈페이지
EG-BIM Q&A 링크] --> B[Q&A 목록 진입
https://qa-test.baroncs.co.kr/egbim/] + B --> C{Q&A 세션 쿠키
baron_qa_session 유효?} + + C -- 예 --> G[정적 목록 페이지 표시] + C -- 아니오 --> D[302 /auth/login
return_url 보관] + D --> E[Worker가 PKCE 생성
state · verifier · nonce] + E --> F[BRSW OIDC SSO
app.brsw.kr/oidc] + F --> F1{중앙 SSO 세션 존재?} + F1 -- 아니오 --> F2[SSO 로그인 화면] + F2 --> F3[최초 동의 화면
필요 시 1회] + F1 -- 예 --> H[승인 코드 발급] + F3 --> H + H --> I[/auth/callback?code&state] + I --> J[Worker가 state 검증
PKCE code_verifier 검증] + J --> K[Token Endpoint 호출
Public Client · client_secret 없음] + K --> L[UserInfo 조회 및 claim 정규화] + L --> M[baron_qa_session 발급
HttpOnly · Secure · SameSite=Lax] + M --> G + + G --> N[문의 등록 버튼] + N --> O[작성 페이지
write.html] + O --> P[GET /auth/session
작성자·tenant 표시] + P --> Q{userUuid와 tenantId 존재?} + Q -- 아니오 --> R[제출 차단
다시 로그인 안내] + Q -- 예 --> S[카테고리·제목·내용·비밀글 입력] + S --> T[등록 버튼 클릭] + T --> U[브라우저 UUID 생성
feedbackId] + U --> V{첨부파일 존재?} + V -- 아니오 --> Y[첨부 메타데이터 빈 배열] + V -- 예 --> W[POST presign 요청] + W --> X[qa_cdn presigned URL로
파일 PUT 업로드] + X --> Y[첨부파일 메타데이터 확정] + Y --> Z[feedback envelope 생성] + Z --> AA{apiBaseUrl 설정 여부} + AA -- 비어 있음
현재 테스트 모드 --> AB[localStorage에 임시 저장] + AA -- 설정됨
실서비스 모드 --> AC[POST /v1/qa/feedbacks
feedback.hmac.kr API] + AB --> AD[등록 완료 toast
상세 페이지 이동] + AC --> AE{API 성공?} + AE -- 예 --> AD + AE -- 아니오 --> AF[오류 표시
재시도 가능] +``` + +## 2. 상태 전이표 + +| 상태 | 화면/주소 | 진입 조건 | 주요 처리 | 다음 상태 | +|---|---|---|---|---| +| `S0` | 홈페이지 | Q&A 메뉴 클릭 | 외부 Q&A URL로 이동 | `S1` | +| `S1` | `/egbim/` | Q&A 세션 유효 | R2의 목록 HTML 제공 | `S5` | +| `S2` | `/auth/login` | 세션 없음 | PKCE 상태 생성, SSO로 redirect | `S3` | +| `S3` | `app.brsw.kr/oidc` | SSO 세션 없음 | 최초 로그인 및 동의 | `S4` | +| `S4` | `/auth/callback` | `code`, `state` 수신 | state/PKCE 검증, token/userinfo 조회 | `S5` | +| `S5` | 목록 또는 작성 페이지 | `baron_qa_session` 유효 | `/auth/session`에서 사용자 확인 | `S6` | +| `S6` | `write.html` | `userUuid`, `tenantId` 확인 | 작성 폼 입력 대기 | `S7` | +| `S7` | 작성 페이지 | 등록 클릭 | `feedbackId` UUID 생성 및 입력 검증 | `S8` | +| `S8` | 작성 페이지 | 첨부 있음 | presign → `qa_cdn` 업로드 | `S9` | +| `S9` | 작성 페이지 | payload 준비 | feedback/support ticket envelope 생성 | `S10` | +| `S10` | API 또는 localStorage | `apiBaseUrl` 상태에 따라 분기 | 실제 DB 저장 또는 테스트 저장 | `S11` | +| `S11` | `detail.html?id=...` | 저장 성공 | 등록 완료 및 상세 이동 | 종료 | + +## 3. 화면별 사용자 경험 + +### A. 홈페이지 + +- EG-BIM 메뉴의 Q&A 링크만 제공한다. +- 로그인 정보, 쿠키, token, `SESSION_SECRET`을 Q&A로 전달하지 않는다. +- 링크 대상: + +```text +https://qa-test.baroncs.co.kr/egbim/ +``` + +### B. 중앙 SSO + +- Q&A RP는 `https://app.brsw.kr/oidc`를 사용한다. +- 최초 사용자는 SSO 로그인 및 동의 화면을 볼 수 있다. +- 같은 중앙 SSO 세션이 있으면 다음 접근부터 로그인 화면 없이 authorization code가 발급된다. +- Q&A RP는 PKCE Public Client이므로 client secret을 보내지 않는다. + +### C. Q&A 목록 + +- Worker가 `baron_qa_session`을 검사한다. +- 세션이 없으면 `/auth/login?return_url=...`로 이동한다. +- 세션이 있으면 R2의 `egbim/index.html`을 제공한다. + +### D. Q&A 작성 + +- 브라우저가 `GET /auth/session`을 호출한다. +- 다음 작성자 정보를 화면과 payload에 사용한다. + +```text +userUuid +ssoSubject / requesterId +tenantId / requesterTenantId +tenantIds +scope +roles +email +name +department +phone +``` + +- `requesterId` 또는 `requesterTenantId`가 없으면 제출을 차단한다. + +### E. 글 저장 + +- 브라우저에서 `feedbackId` UUID를 1회 생성한다. +- 같은 UUID를 다음 식별자로 재사용한다. + +```text +feedback.id +feedback.source_record_id +support_tickets.idempotency_key +``` + +- 첨부파일은 `qa_cdn`에 저장하고, DB에는 bucket/key 및 파일 메타데이터를 기록한다. +- 실제 운영에서는 feedback API가 서버 측에서 Q&A 세션 또는 SSO token을 검증해야 한다. + +## 4. 현재 테스트 모드와 운영 모드 + +현재 `assets/config.js`는 다음과 같이 `apiBaseUrl`이 비어 있다. + +```js +apiBaseUrl: '' +``` + +따라서 현재 등록 결과는 다음과 같이 동작한다. + +```text +글 작성 → UUID 생성 → localStorage 저장 → 상세 페이지 이동 +``` + +이 테스트 모드에서는 ABC DB에 저장되지 않으므로 관리페이지 목록에 나타나지 않고, `FEEDBACK_CREATION` 이벤트와 네이버웍스·SMS·카카오톡 알림도 발생하지 않는다. 관리페이지·알림까지 검증하려면 `apiBaseUrl`을 실제 API로 설정하고, 내부 수신자는 `NAVER_WORKS_ENABLED=true`와 WORKS 계정 매핑을, 외부 수신자는 외부 알림 게이트웨이·전화번호·카카오 템플릿을 준비해야 한다. + +운영 API 주소를 설정하면 다음 흐름으로 변경된다. + +```text +첨부 presign 요청 +→ qa_cdn 직접 업로드 +→ feedback.hmac.kr/v1/qa/feedbacks 호출 +→ feedbacks/support_tickets 트랜잭션 저장 +→ 상세 페이지 이동 +``` + +## 5. 주요 실패 전이 + +| 실패 지점 | 사용자 화면 | 원인/확인 항목 | +|---|---|---| +| SSO 로그인 전 | 로그인 화면 | 중앙 SSO 세션 없음 또는 다른 SSO 환경 | +| callback | `invalid_oauth_state` | OAuth cookie 만료, 호스트 변경, state 불일치 | +| token 교환 | `oauth_token_exchange_failed` | issuer/endpoint/client ID/PKCE 설정 불일치 | +| 세션 확인 | `authenticated:false` | `baron_qa_session` 없음·만료·`SESSION_SECRET` 불일치 | +| 작성자 확인 | UUID/tenant 오류 | SSO scope 또는 claim mapping 누락 | +| 첨부 업로드 | 업로드 실패 | presign API 또는 `qa_cdn` 권한 오류 | +| DB 저장 | API 요청 오류 | `apiBaseUrl`, CORS, API 인증, DB envelope 규격 오류 | + +## 6. 관리페이지와 ABC API 사이의 주고받기 + +Q&A 작성 페이지와 관리페이지는 서로의 화면을 직접 호출하지 않는다. 두 화면 모두 같은 ABC UserFeedback API를 사용하고, `feedbackId`를 공통 식별자로 사용한다. + +```mermaid +sequenceDiagram + participant Q as EG-BIM Q&A + participant A as ABC API
feedback.hmac.kr + participant DB as ABC DB + participant M as 통합 관리페이지
Next.js 서버 프록시 + participant S as Secretary API + + Q->>A: POST feedbacks
x-api-key + feedback envelope + A->>DB: feedback 저장
feedbackId 생성·중복 확인 + A-->>Q: { id: feedbackId } + + M->>A: workspaceCode로 project/channel 매핑 조회 + A-->>M: projectId, channelId + M->>A: POST /api/v2/.../feedbacks/search + A-->>M: feedback 목록(items) + M->>A: GET .../feedbacks/{feedbackId}/comments + A-->>M: 공개 댓글 목록 + + M->>A: PATCH .../feedbacks/{feedbackId}
상태·필드 변경 + A-->>M: 성공 응답 + M->>A: POST .../feedbacks/{feedbackId}/comments
관리자 공개 댓글 + A-->>M: 저장된 comment + M->>S: 담당자 지정·조회 + S-->>M: 담당자·처리 메타데이터 +``` + +### 6.1 관리페이지 진입 시 식별자 매핑 + +관리페이지는 화면의 `workspaceCode`를 ABC의 실제 프로젝트·채널 UUID로 변환한 뒤 요청한다. + +| 단계 | 요청/응답 | 목적 | +|---|---|---| +| 1 | `GET /api/internal/support/workspace-mappings?workspaceCodes={workspaceCode}` | workspace와 `projectId`·`channelId` 매핑 조회 | +| 2 | `x-api-key: MASTER_API_KEY` | 내부 매핑 API 인증 | +| 3 | `SECRETARY_ABC_API_KEY` | 관리페이지 서버가 ABC API를 호출할 때 사용하는 서비스 키 | +| 4 | 매핑 결과 캐시(현재 30초) | 같은 화면에서 반복 매핑 요청 감소 | + +이 매핑이 실패하면 관리페이지는 ABC 피드백을 조회하거나 저장할 수 없다. `workspaceCode`와 ABC의 프로젝트·채널 이름/UUID 매핑을 먼저 확인한다. + +### 6.2 목록·상세 조회 + +현재 관리페이지의 ABC 연동은 Next.js 서버 라우트가 API 키를 보관하고 브라우저 요청을 ABC API로 중계하는 방식이다. + +| 관리 동작 | 관리페이지 서버의 처리 | ABC API 요청 | 응답/화면 반영 | +|---|---|---|---| +| 피드백 목록 | workspace 매핑 후 목록 조회 | `POST /api/v2/projects/{projectId}/channels/{channelId}/feedbacks/search` | `items`를 목록·대시보드에 표시 | +| 피드백 상세 | 동일한 `feedbackId`로 대상 피드백 조회 | `GET /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}` 또는 매핑된 목록 조회 | 제목·내용·작성자·상태·이슈 표시 | +| 공개 댓글 조회 | `includeInternal=false`로 조회 | `GET /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments` | Q&A 작성자에게 공개되는 댓글만 표시 | +| 담당자 조회/지정 | Secretary API로 전달 | `/api/tickets/{ticketId}/assignee` | `assignee_id`, `assignee_tenant_id`, 이름·이메일 표시 | + +관리페이지 목록의 `ticketId` 또는 화면 표시 번호는 화면용 값일 수 있다. ABC DB와 API에서의 정식 식별자는 항상 원래의 `feedbackId` UUID다. + +### 6.3 관리페이지에서 변경하는 데이터 + +#### 상태 변경 + +1. 관리자가 상태를 선택한다. +2. Next.js 서버 라우트가 관리자 세션과 권한을 확인한다. +3. `feedback_status` 필드의 허용 옵션을 확인한다. +4. ABC의 `PATCH /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}`로 상태를 저장한다. +5. 실제 상태가 변경된 경우 ABC가 `FEEDBACK_STATUS_CHANGE` 이벤트를 발생시킨다. +6. 관리페이지는 저장된 상태로 화면을 갱신하고, WORKS 알림은 별도 비동기 흐름으로 처리한다. + +관리페이지 프록시 경로는 다음과 같다. + +```text +PATCH /api/support/tickets/{ticketId}/feedback-status + → updateAbcSupportFeedbackStatus() + → PATCH /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId} +``` + +#### 담당자 지정 + +담당자 정보는 `assignee_id`, `assignee_tenant_id`, `assignee_name`, `assignee_email`을 사용한다. 관리페이지는 담당자 지정 요청을 Secretary API로 보내고, 이후 목록·상세 조회에서 해당 값을 다시 읽는다. + +담당자 이메일은 WORKS 알림 수신자 결정에도 사용되므로, 담당자 변경 후에는 이메일과 WORKS 계정 매핑이 함께 유효한지 확인한다. + +#### 공개 댓글·완료 안내 + +관리자 댓글은 다음 데이터로 ABC에 저장한다. + +```json +{ + "author_id": "관리자 SSO user id", + "author_tenant_id": "관리자 tenant id", + "author_name": "관리자 표시명", + "content": "작성자에게 보여줄 답변", + "is_internal": false, + "actor_type": "ADMIN", + "comment_type": "COMMENT" +} +``` + +관리자 공개 댓글은 `POST /api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments`로 저장된다. `comment_type`을 `COMPLETION_NOTICE`로 저장하면 완료 안내로 취급하고, 연결된 이슈가 모두 완료된 경우 작성자의 완료 확인 단계로 넘어간다. + +내부 메모는 공개 댓글과 다른 데이터다. Q&A 작성자에게 보여서는 안 되며, 현재 WORKS 일반 사용자 알림 대상에서도 제외한다. + +### 6.4 작성자와 관리페이지의 왕복 + +처리 결과가 작성자에게 돌아가는 경로는 다음과 같다. + +```text +관리자 공개 댓글 저장 +→ Q&A 상세 페이지에서 댓글 재조회 +→ 관리자가 COMPLETION_NOTICE를 남긴 경우 완료 확인 버튼 노출 +→ 작성자 POST /feedbacks/{feedbackId}/completion-confirmation +→ requester_id·requester_tenant_id 검증 +→ 연결 이슈 완료 여부 확인 +→ feedback_status를 완료로 변경 +→ 상태 변경 이벤트 및 WORKS 알림 +``` + +작성자의 추가 댓글은 같은 댓글 API로 저장된다. 작성자 댓글 이벤트가 발생하면 담당자와 프로젝트 관리자에게 WORKS 알림을 보내고, 완료 안내 대기 중이었다면 재문의 상태를 기록한다. 완료된 피드백을 작성자 댓글만으로 자동 재오픈하지는 않는다. + +## 7. 이벤트와 사용자 유형별 알림 전이 + +피드백 저장 응답과 알림 발송 성공은 같은 단계가 아니다. ABC는 먼저 DB 저장 결과를 Q&A/관리페이지에 반환하고, 이벤트 리스너가 수신자의 사용자 유형을 판정한 뒤 알림 채널을 나누어 발송한다. + +```mermaid +flowchart TD + A[ABC feedback 저장 또는 관리자 변경] --> B[MySQL 저장] + B --> C[EventEmitter 이벤트 발생] + C --> D{이벤트 종류} + + D -->|FEEDBACK_CREATION| E[신규 피드백 수신자 결정] + D -->|FEEDBACK_STATUS_CHANGE| F[상태 변경 전·후 확인] + D -->|FEEDBACK_COMMENT_CREATION| G[댓글 작성자 유형 확인] + + E --> H[담당자·작성자·프로젝트 관리자 중복 제거] + F --> H + G -->|관리자 공개 댓글| I[작성자 1명] + G -->|작성자 댓글| J[담당자·프로젝트 관리자] + G -->|내부 메모| K[알림 제외] + + H --> L{수신자 사용자 유형 판정} + I --> L + J --> L + L -->|사내 사용자·관리자·담당자| M[이메일로 WORKS userId 조회] + M --> N[NAVER WORKS Bot API
사용자별 메시지] + L -->|외부 Q&A 작성자| O[카카오 알림톡 또는 SMS 발송] + N --> P[알림 발송 이력 저장] + O --> P + P --> Q{SENT / FAILED / SKIPPED} +``` + +### 7.1 이벤트별 수신자 + +| ABC 이벤트 | 발생 시점 | 사내 수신자: 네이버웍스 | 외부 작성자: SMS·카카오톡 | +|---|---|---|---| +| `FEEDBACK_CREATION` | Q&A 글 DB 저장 완료 | 담당자, 프로젝트 관리자, 작성자가 사내 사용자인 경우 | 외부 작성자에게 접수 완료 알림 | +| `FEEDBACK_STATUS_CHANGE` | 상태가 실제로 달라짐 | 담당자, 프로젝트 관리자, 작성자가 사내 사용자인 경우 | 외부 작성자에게 이전 상태 → 현재 상태 알림 | +| `FEEDBACK_COMMENT_CREATION` + 관리자 | 관리자가 공개 댓글 저장 | 수신 대상이 사내 작성자이면 WORKS | 외부 작성자에게 답변 등록 알림 | +| `FEEDBACK_COMMENT_CREATION` + 작성자 | 작성자가 추가 댓글 저장 | 담당자, 프로젝트 관리자 | 외부 작성자에게는 별도 알림 없음 | +| 내부 메모 | 내부 협업 메모 저장 | 발송하지 않음 | 외부 작성자에게 절대 노출하지 않음 | + +같은 사람이 여러 역할을 가지고 있어도 채널별 한 번만 발송한다. 수신자는 이벤트 payload의 임의 이메일이나 전화번호를 그대로 신뢰하지 않고, 피드백의 담당자·작성자·프로젝트 권한과 사용자 유형을 서버에서 다시 조회해 결정한다. + +### 7.2 알림 채널 분류 + +알림 채널은 역할명이 아니라 수신자의 사용자 유형으로 결정한다. + +| 수신자 유형 | 대상 | 기본 채널 | 식별·연락처 기준 | +|---|---|---|---| +| 사내 사용자 | Q&A 작성자 중 회사 내부 사용자 | 네이버웍스 | SSO subject·tenant·회사 소속 정보, WORKS 계정 이메일 | +| 관리자 | 시스템 관리자·프로젝트 관리자 | 네이버웍스 | 내부 권한과 활성 계정, WORKS 계정 이메일 | +| 담당자 | 피드백 담당자 | 네이버웍스 | `assignee_id`·`assignee_tenant_id`, `assignee_email` | +| 외부 사용자 | 회사 외부 Q&A 작성자 | 카카오 알림톡 또는 SMS | 인증된 작성자 프로필의 `requester_phone_number` | + +외부 사용자는 네이버웍스 계정이 없다는 이유만으로 분류하지 않는다. SSO의 테넌트·회사 소속 정보와 서비스의 사용자 분류를 기준으로 외부 여부를 판정한다. 외부 작성자에게는 네이버웍스 메시지를 보내지 않는다. + +### 7.3 사내 사용자·관리자·담당자: WORKS 발송 + +```text +이벤트 수신 +→ idempotency key 중복 확인 +→ 사내 수신자만 WORKS 대상자로 분류 +→ SSO 이메일을 NAVER WORKS userId로 조회 +→ Bot API 사용자 메시지 발송 +→ WORKS 발송 결과 기록 +``` + +- 사용자별 메시지 경로를 사용하며, 수신자 매핑 실패를 전체 방 발송으로 대체하지 않는다. +- `NAVER_WORKS_ENABLED=false`이면 발송하지 않는다. +- access token은 직접 설정된 값 또는 JWT bearer 방식으로 발급받는다. +- 네트워크 오류, `408`, `429`, `5xx`는 설정된 횟수까지 재시도한다. +- 중복 이벤트는 `eventId` 또는 이벤트·피드백·댓글 조합의 idempotency key로 차단한다. +- WORKS 발송 상태는 `RECEIVED`, `SENT`, `FAILED`, `SKIPPED`로 기록한다. + +### 7.4 외부 사용자: 카카오톡·SMS 발송 + +외부 작성자 알림은 외부 알림 게이트웨이를 통해 처리한다. 카카오톡은 거래성 안내에 적합한 승인 템플릿이 준비된 경우 알림톡을 우선 사용하고, 카카오톡 발송 불가·수신자 미매핑·실패 시 SMS로 대체하는 방식을 기본안으로 둔다. + +```mermaid +flowchart LR + A[외부 작성자 대상 이벤트] --> B[requester_phone_number 확인] + B --> C{카카오 알림톡 사용 가능?} + C -->|예| D[카카오 알림톡 발송] + C -->|아니오| E[SMS 발송] + D --> F{성공?} + F -->|예| G[발송 완료 기록] + F -->|아니오| E + E --> H{성공?} + H -->|예| G + H -->|아니오| I[실패·재처리 대상 기록] +``` + +외부 알림 발송 순서는 다음과 같다. + +```text +이벤트 수신 +→ requester가 외부 사용자임을 서버에서 판정 +→ 인증된 requester_phone_number 조회 +→ 카카오 알림톡 템플릿·발송 가능 여부 확인 +→ 카카오 알림톡 발송 +→ 실패 또는 미사용 시 SMS fallback +→ 채널별 발송 결과와 실패 사유 기록 +``` + +- 전화번호가 없거나 검증되지 않은 경우 임의 번호로 보내지 않고 `SKIPPED` 또는 `FAILED`로 남긴다. +- 외부 알림에는 내부 메모, SSO token, 관리자 URL, WORKS 인증정보를 포함하지 않는다. +- 비밀글은 제목·본문·댓글 원문을 넣지 않고, Q&A 상세 페이지에서 권한을 확인할 수 있는 최소 안내만 보낸다. +- 외부 사용자에게 보내는 링크는 관리페이지가 아니라 Q&A 상세 페이지 링크를 사용한다. +- 카카오 알림톡 템플릿 ID, SMS 발신번호, 통신사/메시지 공급자 설정은 운영 환경별 비밀 설정으로 관리한다. +- 외부 알림도 이벤트별·수신자별 idempotency key를 사용해 카카오톡과 SMS가 중복 발송되지 않도록 한다. + +현재 저장소에는 WORKS 발송 어댑터가 구현되어 있지만, 외부 사용자용 카카오톡·SMS 발송 어댑터와 수신자 유형별 분기 로직은 별도 구현 범위로 관리해야 한다. 특히 현재 `NotificationRecipientRouter`가 생성·상태 변경 이벤트에서 `requester_email`을 WORKS 대상에 포함하므로, 외부 작성자를 WORKS에서 제외하고 외부 알림 라우터로 보내는 변경이 필요하다. + +### 7.5 WORKS 메시지와 외부 알림의 개인정보 보호 + +- 일반 관리자 수신자에게는 피드백 ID·제목·상태·댓글 일부·관리페이지 상세 링크를 보낸다. +- 외부 작성자에게는 피드백 ID·상태·답변 등록 여부 등 최소 정보와 Q&A 상세 링크만 보낸다. +- 비밀글 작성자에게는 본문·제목·댓글 원문을 보내지 않고, 권한 검증이 필요한 Q&A 상세 링크만 보낸다. +- SSO token, Webhook token, WORKS access token·client secret·private key는 메시지나 로그에 기록하지 않는다. +- 메시지 최대 길이는 현재 `NAVER_WORKS_MAX_MESSAGE_LENGTH` 설정값(기본 1,000자)을 따른다. + +외부 알림에는 별도의 채널별 최대 길이와 템플릿 버전을 둔다. `notification_deliveries`를 확장하거나 별도 외부 알림 이력 테이블을 두어 다음 정보를 남기는 것을 권장한다. + +```text +recipient_type: INTERNAL | EXTERNAL +channel: NAVER_WORKS | KAKAO_ALIMTALK | SMS +recipient_id: 내부 userId 또는 마스킹된 전화번호 식별자 +template_id / template_version +status: RECEIVED | SENT | FAILED | SKIPPED +attempts / last_error / sent_at +``` + +### 7.6 프로젝트 Webhook과 알림 채널의 구분 + +아래 세 경로는 이름이 비슷하지만 역할이 다르다. + +| 경로 | 방향 | 용도 | WORKS 발송과의 관계 | +|---|---|---|---| +| 프로젝트 Webhook 설정 | 관리페이지 → ABC | 프로젝트별 외부 Webhook URL·이벤트·채널 범위 관리 | 외부 시스템 연동용. WORKS 직접 발송과 별도 | +| `POST /integrations/abc/webhooks` | 외부 시스템 → API | 인증된 ABC Webhook 이벤트를 수신해 알림 채널 라우터로 전달 | `ABC_WEBHOOK_ENABLED`가 켜진 외부 수신 경로 | +| `NotificationInternalListener` | ABC API 내부 이벤트 → 알림 라우터 | Q&A/관리자 변경 직후 수신자 유형에 따라 WORKS·외부 채널로 라우팅 | 현재 Q&A 저장·관리 변경의 기본 경로 | + +따라서 같은 API 프로세스 안에서 발생한 피드백 생성·상태 변경·댓글 이벤트의 알림은 외부 Webhook 왕복을 전제로 하지 않는다. `ABC_WEBHOOK_ENABLED`는 외부 Webhook 수신을 사용할 때 필요한 설정이고, 내부 수신자는 `NAVER_WORKS_ENABLED`와 WORKS 인증·수신자 매핑을, 외부 수신자는 외부 알림 게이트웨이와 전화번호·템플릿 설정을 확인한다. + +## 8. 전이 상태 요약 + +| 상태 | 화면/시스템 | 진입 이벤트 | 주고받는 데이터 | 다음 상태 | +|---|---|---|---|---| +| `S12` | 관리페이지 목록 | workspace 진입 | workspaceCode ↔ projectId/channelId, feedback search | `S13` | +| `S13` | 관리페이지 상세 | 목록에서 feedbackId 선택 | 피드백 본문·작성자·상태·이슈·공개 댓글 | `S14` | +| `S14` | ABC 피드백 저장 | Q&A 등록 또는 관리 변경 | feedbackId, dynamic fields, source/idempotency key | `S15` | +| `S15` | 이벤트 처리 | 생성·상태·댓글 이벤트 | 이벤트 타입, projectId, channelId, feedbackId | `S16` | +| `S16` | 수신자 유형 판정 | 사내 사용자·관리자·담당자 또는 외부 작성자 식별 | user type, assignee/requester/project admin | `S17` 또는 `S18` | +| `S17` | WORKS 발송 기록 | 사내 userId 조회 및 Bot API 호출 | channel, target userId, status, attempts, error | 종료 또는 재시도 | +| `S18` | SMS·카카오톡 발송 기록 | 외부 전화번호·템플릿 조회 및 발송 | channel, masked phone, template, status, attempts | 종료 또는 재시도 | +| `S19` | 작성자 완료 확인 | 공개 완료 안내 후 작성자 확인 | requesterId, requesterTenantId, feedbackId | 완료 상태 및 `S15` | + +## 9. 운영 전 검증 체크리스트 + +- [ ] `workspaceCode`가 정확한 ABC `projectId`·`channelId`로 매핑된다. +- [ ] Q&A 저장 결과의 `feedbackId`가 관리페이지 상세의 `feedbackId`와 같다. +- [ ] 관리페이지 목록·상세에서 제목·내용·작성자·비밀글 여부·상태·첨부가 일치한다. +- [ ] 관리자가 상태를 바꾸면 ABC 상태가 바뀌고, 같은 상태 재저장에는 중복 알림이 없다. +- [ ] 관리자 공개 댓글이 Q&A 상세에서 보이고, 내부 메모는 보이지 않는다. +- [ ] `COMPLETION_NOTICE` 저장 후 작성자만 완료 확인을 할 수 있다. +- [ ] 신규 피드백 알림이 사내 담당자·프로젝트 관리자에게 WORKS로 중복 없이 도착한다. +- [ ] 사내 작성자에게는 WORKS로, 외부 작성자에게는 SMS·카카오톡으로 알림이 간다. +- [ ] 관리자 공개 댓글 알림은 외부 작성자에게 SMS·카카오톡으로 도착한다. +- [ ] 작성자 추가 댓글 알림은 사내 담당자·프로젝트 관리자에게 WORKS로 도착한다. +- [ ] 비밀글의 본문·제목·댓글 원문이 WORKS 메시지에 노출되지 않는다. +- [ ] WORKS 계정 매핑 실패 시 전체 방으로 오발송되지 않고 `FAILED` 또는 `SKIPPED`로 남는다. +- [ ] 외부 전화번호 매핑 실패 시 임의 번호로 보내지 않고 `FAILED` 또는 `SKIPPED`로 남는다. +- [ ] 카카오 알림톡 실패 시 SMS fallback이 한 번만 수행된다. +- [ ] WORKS·카카오톡·SMS의 발송 성공·실패·재시도 횟수를 채널별로 확인할 수 있다. diff --git a/docs/관리페이지 md 파일/# EG-BIM Q&A 사용자 여정 맵.md b/docs/관리페이지 md 파일/# EG-BIM Q&A 사용자 여정 맵.md new file mode 100644 index 0000000..9e85b83 --- /dev/null +++ b/docs/관리페이지 md 파일/# EG-BIM Q&A 사용자 여정 맵.md @@ -0,0 +1,198 @@ +# EG-BIM Q&A 사용자 여정 맵 + +기준일: 2026-09-21 +대상 서비스: `qa-test.baroncs.co.kr` / EG-BIM Q&A +주요 사용자: EG-BIM 관련 문의가 있는 작성자, 문의를 처리하는 관리자·담당자 +여정 범위: Q&A 진입부터 문의 등록, 답변 확인, 완료 확인까지 + +이 문서는 시스템 상태의 흐름이 아니라 사용자가 경험하는 목표·행동·감정·접점·문제·개선 기회를 중심으로 정리한 journey map이다. 상세한 OAuth, API, DB 상태 전이는 [기술 전이맵](<./# EG-BIM Q&A 로그인부터 글 저장까지 전이맵.md>)을 참고한다. + +## 1. 한눈에 보는 여정 + +```mermaid +journey + title EG-BIM Q&A 문의 작성자 여정 + section 문의를 시작함 + Q&A 메뉴를 발견한다: 4: 작성자 + Q&A 페이지로 이동한다: 4: 작성자 + SSO 로그인·동의를 완료한다: 2: 작성자 + section 문의를 등록함 + 목록에서 문의 등록을 선택한다: 4: 작성자 + 카테고리·제목·내용을 입력한다: 4: 작성자 + 첨부파일과 비밀글 여부를 설정한다: 3: 작성자 + 문의를 제출하고 접수 결과를 확인한다: 4: 작성자 + section 처리 결과를 기다림 + 문의 상세에서 접수 내용을 확인한다: 3: 작성자 + 상태 변경·답변 알림을 받는다: 3: 작성자 + 상세 페이지에서 관리자 답변을 확인한다: 4: 작성자 + section 문의를 마무리함 + 추가 질문을 남긴다: 3: 작성자 + 완료 안내를 확인한다: 4: 작성자 + 완료 확인을 제출한다: 4: 작성자 +``` + +## 2. 사용자 프로필과 여정의 목표 + +### 문의 작성자 + +EG-BIM을 사용하면서 문제가 생겼거나 지원이 필요한 사용자다. 빠르게 문의를 남기고, 내 문의가 정상적으로 접수되었는지와 현재 처리 상태를 알고 싶어 한다. 답변이 도착하면 내용을 확인하고 필요할 경우 추가 질문을 남긴다. + +사용자의 핵심 목표는 다음과 같다. + +- 별도의 복잡한 가입 절차 없이 Q&A에 진입하기 +- 문의 내용을 정확하게 전달하기 +- 첨부파일과 비밀글을 안전하게 사용하기 +- 문의가 접수되고 처리 중인지 확인하기 +- 답변을 확인하고 문제 해결 여부를 마무리하기 + +### 관리자·담당자 + +접수된 문의를 확인하고 담당자를 지정한 뒤 상태, 공개 답변, 내부 메모를 관리한다. 작성자에게 필요한 정보만 전달하면서 내부 협업 정보와 비밀 정보를 보호해야 한다. + +## 3. 문의 작성자 여정 맵 + +| 단계 | 사용자의 목표·질문 | 사용자 행동 | 주요 접점 | 생각·감정 | 문제점·불안 요소 | 개선 기회 | +|---|---|---|---|---|---|---| +| 1. 진입 | “어디에서 문의하지?” | 홈페이지에서 EG-BIM Q&A 메뉴를 찾고 클릭한다. | 바론 홈페이지, Q&A 링크 | 빠르게 해결하고 싶음 | Q&A 위치나 별도 사이트 이동이 명확하지 않을 수 있음 | 메뉴명에 `Q&A 문의하기`를 사용하고, 이동 전 안내 문구를 제공한다. | +| 2. 인증 | “내 계정으로 바로 들어갈 수 있나?” | Q&A로 이동한 뒤 필요하면 중앙 SSO에서 로그인·동의한다. | Q&A 진입 화면, BRSW SSO | 기대 → 로그인 피로 | 외부 사이트로 이동한 이유를 모를 수 있고, 로그인 실패 원인이 불명확할 수 있음 | `EG-BIM 계정으로 로그인합니다`라는 설명과 실패 원인별 안내를 제공한다. | +| 3. 문의 시작 | “새 문의를 어디서 작성하지?” | 목록을 확인하고 문의 등록 버튼을 누른다. | Q&A 목록, 문의 등록 버튼 | 안도감 | 버튼이 눈에 띄지 않거나 기존 문의와 새 문의의 구분이 약할 수 있음 | 목록 상단에 주요 CTA를 고정하고 `새 문의 등록`으로 명확히 표시한다. | +| 4. 내용 작성 | “무엇을 얼마나 적어야 하지?” | 카테고리, 제목, 내용, 비밀글 여부를 입력한다. | 작성 폼, 입력 도움말 | 집중, 약간의 부담 | 필수 항목, 적절한 설명 수준, 개인정보 입력 여부를 판단하기 어려울 수 있음 | 예시 문구, 필수 표시, 개인정보·비밀글 안내, 작성 중 이탈 방지를 제공한다. | +| 5. 첨부·제출 | “파일이 제대로 올라갔나? 제출됐나?” | 파일을 첨부하고 등록 버튼을 누른다. | 첨부 UI, 업로드 진행 표시, 등록 버튼 | 긴장 → 안도 | 큰 파일·지원하지 않는 형식·네트워크 오류를 알기 어려울 수 있음 | 파일 형식·용량 사전 안내, 진행률, 재시도, 중복 제출 방지를 제공한다. | +| 6. 접수 확인 | “내 문의가 접수됐나?” | 등록 완료 메시지와 문의 상세를 확인한다. | 완료 toast, 상세 페이지 | 안도감 | 접수 번호와 다음 단계가 충분히 안내되지 않을 수 있음 | 접수 번호, 현재 상태, 예상 처리 안내, 알림 수단을 명확히 보여준다. | +| 7. 처리 대기 | “누가 보고 있나? 언제 답이 오나?” | 상세 페이지를 다시 방문하거나 알림을 확인한다. | Q&A 상세, SMS·카카오 알림 | 불확실함 | 처리 상태가 오래 바뀌지 않거나 진행 상황을 알 수 없을 수 있음 | 상태 정의, 최종 업데이트 시각, 예상 응답 시간, 상태 변경 알림을 제공한다. | +| 8. 답변 확인 | “문제가 해결됐나?” | 관리자 공개 댓글과 상태를 확인한다. | 상세 페이지, 답변 알림 | 기대 → 안도 또는 추가 질문 | 답변과 내부 메모가 섞이거나, 답변 알림에서 내용을 확인하기 어려울 수 있음 | 공개 답변만 노출하고, 답변 알림은 상세 페이지로 연결한다. | +| 9. 추가 질문 | “조금 더 설명하거나 다시 물어봐도 되나?” | 추가 댓글을 남기고 필요한 파일을 보완한다. | 댓글 입력, 첨부 UI | 협업, 때로는 답답함 | 완료된 문의를 다시 문의해야 하는지 기준이 모호할 수 있음 | `추가 문의`와 `완료 확인`을 구분하고, 완료 후 재문의 정책을 안내한다. | +| 10. 완료 | “이 문의를 끝내도 되나?” | 완료 안내를 확인하고 완료 확인을 제출한다. | 완료 안내, 완료 확인 버튼 | 마무리, 만족 또는 망설임 | 완료 확인의 의미와 취소 가능 여부가 불명확할 수 있음 | 완료 확인 전 안내, 확인 후 상태·후속 문의 방법을 제공한다. | + +## 4. 감정 곡선과 핵심 순간 + +```text +감정 +높음 진입 ── 문의 시작 ── 접수 확인 ───────── 답변 확인 ── 완료 + ︿ ︿ ︿ ︿ ︿ +중간 ────┘ └─ 작성 ──────┘ └─ 추가 질문 ┘ +낮음 인증 실패·업로드 실패·장기 대기 +``` + +가장 중요한 순간은 다음 네 가지다. + +1. **로그인 전환 순간**: 사용자는 Q&A가 다른 사이트로 보이더라도 인증 과정이 안전하고 자연스럽다고 느껴야 한다. +2. **제출 직후**: 문의가 실제로 접수되었다는 확신이 필요하다. 완료 메시지만이 아니라 접수 번호와 현재 상태가 중요하다. +3. **처리 대기 중**: 답변이 없더라도 문의가 사라지지 않았다는 신뢰를 유지해야 한다. +4. **답변·완료 순간**: 공개 답변과 내부 처리 정보를 구분하고, 사용자가 해결 여부를 명확히 표시할 수 있어야 한다. + +## 5. 관리자·담당자 백스테이지 여정 + +| 단계 | 관리자·담당자의 행동 | 사용자에게 보이는 결과 | 백스테이지 시스템 | 주의점 | +|---|---|---|---|---| +| 접수 확인 | 새 문의 목록을 확인한다. | 문의가 목록과 상세에 표시된다. | ABC API, workspace → project/channel 매핑 | `feedbackId`를 화면 표시 번호와 혼동하지 않는다. | +| 담당자 지정 | 담당자와 처리 범위를 지정한다. | 필요하면 담당자 지정 상태가 표시된다. | Secretary API, ABC feedback 필드 | 담당자 이메일과 WORKS 계정 매핑을 함께 확인한다. | +| 상태 관리 | 접수·처리 중·완료 등 상태를 변경한다. | 상태와 최종 변경 시각이 갱신된다. | ABC API PATCH, 상태 변경 이벤트 | 같은 상태를 다시 저장해 중복 알림을 만들지 않는다. | +| 내부 협업 | 내부 메모를 남긴다. | 작성자에게 노출되지 않는다. | 내부 댓글·권한 처리 | 내부 메모가 공개 댓글로 저장되지 않도록 구분한다. | +| 공개 답변 | 작성자에게 보여줄 댓글을 등록한다. | Q&A 상세에서 답변을 볼 수 있다. | 공개 댓글 API, 댓글 생성 이벤트 | 비밀글 내용과 내부 정보가 알림에 포함되지 않게 한다. | +| 완료 안내 | 완료 안내를 남기고 해결 여부를 요청한다. | 작성자에게 완료 확인 버튼이 노출된다. | `COMPLETION_NOTICE`, requester 검증 | 작성자 본인만 완료 확인할 수 있어야 한다. | +| 알림 처리 | 대상에 맞는 채널로 알림을 보낸다. | 사내 사용자는 WORKS, 외부 사용자는 카카오·SMS를 받는다. | 이벤트 라우터, 알림 이력 | 사용자 유형을 이메일만으로 판단하지 않고 서버에서 재검증한다. | + +## 6. 터치포인트와 책임 주체 + +| 터치포인트 | 사용자 경험 책임 | 주요 성공 기준 | +|---|---|---| +| 홈페이지 Q&A 링크 | 홈페이지 | 링크가 눈에 띄고 이동 목적이 명확하다. | +| SSO 로그인·동의 | 인증/플랫폼 | 로그인 성공률이 높고 실패 이유를 이해할 수 있다. | +| Q&A 목록 | Q&A 프론트엔드 | 문의 등록과 기존 문의 확인이 쉽다. | +| 작성 폼 | Q&A 프론트엔드 | 필수 입력·첨부·비밀글 설정을 오류 없이 완료한다. | +| 등록 API·파일 업로드 | Q&A API·스토리지 | 중복 없이 저장되고 실패 시 복구 경로가 있다. | +| 관리페이지 | 운영/관리자 콘솔 | 문의가 빠르게 분류·담당 배정·처리된다. | +| 공개 댓글 | 관리자·Q&A | 작성자가 이해할 수 있는 답변이 제공된다. | +| WORKS·카카오·SMS | 알림 시스템 | 올바른 수신자에게 한 번만, 안전한 내용으로 전달된다. | +| 완료 확인 | Q&A·ABC API | 작성자 본인의 확인만 처리되고 상태가 일관되게 갱신된다. | + +## 7. 핵심 개선 과제 우선순위 + +| 우선순위 | 개선 과제 | 기대 효과 | 확인 지표 | +|---|---|---|---| +| P0 | 등록 성공 화면에 접수 번호·상태·다음 행동을 명확히 표시 | 제출 후 불안 감소, 중복 문의 감소 | 등록 후 재제출 비율, 접수 확인 관련 문의 수 | +| P0 | 로그인·세션·첨부 업로드 실패 메시지를 사용자 언어로 정리 | 이탈과 반복 시도 감소 | 인증/업로드 실패 후 이탈률 | +| P0 | 공개 댓글·내부 메모·외부 알림의 정보 경계를 검증 | 정보 노출 사고 방지 | 권한 테스트, 알림 payload 점검 | +| P1 | 처리 상태와 최종 업데이트 시각을 상세에 표시 | 처리 대기 중 신뢰 향상 | 상세 재방문율, 상태 문의 건수 | +| P1 | 카카오 알림톡 실패 시 SMS fallback과 발송 결과를 운영 화면에 표시 | 답변 도달률 향상 | 채널별 SENT·FAILED·SKIPPED 비율 | +| P1 | 완료 안내와 추가 문의의 행동을 분리 | 완료 후 혼란 감소 | 완료 확인율, 완료 후 재문의 처리시간 | +| P2 | 작성 예시·첨부 가이드·비밀글 안내를 폼에 추가 | 문의 품질 향상 | 추가 확인 요청 비율, 첨부 오류율 | + +## 8. 여정에서 보장해야 하는 원칙 + +- 사용자는 자신의 문의와 공개 답변만 볼 수 있어야 한다. +- 내부 메모, SSO token, API key, WORKS 인증정보는 사용자 화면·알림·로그에 노출하지 않는다. +- 비밀글은 알림에 제목·본문·댓글 원문을 포함하지 않고, 상세 페이지에서 권한을 확인하도록 한다. +- 접수 성공과 알림 발송 성공은 별개의 결과로 보여준다. 문의가 저장되었지만 알림이 실패할 수 있다. +- 같은 사용자가 여러 역할을 가져도 한 이벤트의 알림은 채널별 한 번만 발송한다. +- 작성자에게 보내는 링크는 관리페이지가 아니라 Q&A 상세 페이지여야 한다. +- 완료 상태는 작성자 본인 확인과 관리자 처리 결과가 일관되게 반영되어야 한다. + +## 9. 검증용 대표 시나리오 + +### 정상 여정 + +```text +홈페이지 Q&A 클릭 +→ SSO 인증 +→ Q&A 목록 +→ 문의 작성·첨부 +→ 등록 성공 및 상세 이동 +→ 관리자 담당자 지정·상태 변경 +→ 관리자 공개 답변 +→ 작성자 답변 확인 +→ 완료 확인 +``` + +### 인증 세션이 이미 있는 사용자 + +```text +홈페이지 Q&A 클릭 +→ 로그인 화면 없이 Q&A 목록 진입 +→ 문의 작성·등록 +``` + +### 첨부 업로드 실패 + +```text +문의 내용 작성 +→ 파일 업로드 실패 +→ 실패 원인 표시·재시도 +→ 성공 후 제출 가능 +``` + +### 외부 작성자 알림 + +```text +문의 저장 +→ 외부 작성자 판정 +→ 카카오 알림톡 시도 +→ 실패 시 SMS fallback +→ 채널별 결과 기록 +``` + +### 내부 메모와 공개 답변 구분 + +```text +관리자 내부 메모 저장 +→ 작성자에게 미노출 + +관리자 공개 답변 저장 +→ 작성자 상세 페이지에 노출 +→ 답변 알림 발송 +``` + +## 10. 현재 테스트 모드에서의 범위 + +현재 `assets/config.js`의 `apiBaseUrl`이 비어 있으면 문의 등록 결과는 브라우저 `localStorage`에 임시 저장된다. 따라서 사용자는 등록 완료와 상세 이동을 경험할 수 있지만, 다음 백스테이지 여정은 실제로 실행되지 않는다. + +- ABC DB 저장 +- 관리페이지 목록 반영 +- 담당자 지정과 상태 변경 +- `FEEDBACK_CREATION` 등 이벤트 발생 +- WORKS·카카오·SMS 알림 +- 작성자 완료 확인의 서버 검증 + +운영 여정을 검증하려면 실제 API 연결, Q&A 세션 검증, 알림 채널 설정, 수신자 매핑, 공개 댓글·내부 메모 권한 검증을 함께 완료해야 한다. + diff --git a/docs/관리페이지 md 파일/BARON-SSO server-side-app guide.md b/docs/관리페이지 md 파일/BARON-SSO server-side-app guide.md new file mode 100644 index 0000000..af70add --- /dev/null +++ b/docs/관리페이지 md 파일/BARON-SSO server-side-app guide.md @@ -0,0 +1,129 @@ +# Baron SSO Server-Side App Demo (Express.js) + +이 프로젝트는 `baron-sso`의 `server-side-app` RP를 테스트하기 위한 단순한 Express.js 데모입니다. + +## 목적 + +이 데모는 다음을 확인하기 위한 용도입니다. + +1. confidential client 기반 OIDC Authorization Code 로그인 +2. RP 로컬 세션 생성 및 유지 +3. `Back-Channel Logout URI` 호출 수신 +4. `logout_token` 검증 후 로컬 세션 즉시 파기 +5. `BARON_SESSION_VALIDATION_ENABLED=false`일 때 access token 만료 후 refresh token 갱신으로 세션 종료 확인 + +## 이 프로젝트 적용 원칙 + +- BARON-SSO는 인증과 현재 테넌트 문맥 확인까지만 사용합니다. +- 최종 권한 부여, 관리자 여부 판정, 프로젝트 접근 제어는 모두 내부 DB에서 처리합니다. +- 따라서 OIDC 로그인 완료 후 RP 세션에는 최소한의 사용자 식별자와 `tenant_id` 만 저장하고, 이후 내부 사용자 매핑과 권한 조회를 별도 계층에서 수행하는 구성이 적합합니다. +- 초기 운영 역할은 `PROJECT_MANAGER` 중심으로 두고, 채널 관리자 역할은 추후 필요 시 확장합니다. +- 관리자 권한 설정 페이지는 관리자 콘솔 메뉴에 추가하는 방향을 기준으로 합니다. +- 사용자 피드백에는 비밀글 기능을 추가하고, 조회 제한은 내부 DB 권한 정책으로 제어합니다. + +## 이 저장소 기준 BARON-SSO 연결값 + +관리자 콘솔의 테넌트 OAuth 설정에는 아래 값을 기준으로 입력합니다. + +```text +Login Type: OAuth 2.0 +Login Button Type: CUSTOM +Login Button Name: BARON SSO 로그인 +Client ID: 838cd69d-e722-41da-9f79-b3c42a509ef2 +Client Secret: +Authorization Code Request URL: https://sso.hmac.kr/oidc/oauth2/auth +Access Token Request URL: https://sso.hmac.kr/oidc/oauth2/token +User Profile Request URL: https://sso.hmac.kr/oidc/userinfo +Scope: openid profile email +Email Key in Response of User Profile: email +Subject Key in Response of User Profile: sub +Name Key in Response of User Profile: name +Department Key in Response of User Profile: department +Redirect URI: http://localhost:3003/api/auth/baron-sso/callback +``` + +설정 메모: + +- 내부 권한 매핑 기준 키는 전화번호가 아니라 `sub`입니다. +- `name`, `department`는 BARON userinfo 응답에 실제로 존재할 때만 화면 표시용으로 사용합니다. +- 테넌트 식별, 관리자 여부, 프로젝트 권한은 BARON에서 결정하지 않고 내부 DB에서 결정합니다. +- `Client Secret`은 저장소에 하드코딩하지 말고 관리자 콘솔에서 직접 입력합니다. + +## 사전 준비 + +1. `baron-sso` 프로젝트가 실행 중이어야 합니다. +2. `baron_net` 네트워크가 생성되어 있어야 합니다. +3. devfront에서 `server-side-app` 타입 RP를 생성해야 합니다. + +## 권장 RP 설정 + +예시: + +```text +Type: server-side-app +Client ID: <생성된 client id> +Client Secret: <생성된 secret> +Redirect URI: http://localhost:4444/callback +Back-Channel Logout URI: http://172.16.x.x:4444/backchannel-logout +SID Claim Required: off +``` + +주의: + +- `Back-Channel Logout URI`는 브라우저 기준이 아니라 Baron backend가 실제로 접근 가능한 주소여야 합니다. +- Docker 환경에서 `localhost`는 backend 컨테이너 자신을 가리킬 수 있으므로, 필요하면 사설 IP 또는 Docker 서비스명을 사용해야 합니다. + +## 실행 + +```bash +docker-compose up --build +``` + +## 환경 변수 + +- `PORT`: 기본값 `4444` +- `SESSION_SECRET`: Express session secret +- `OIDC_ISSUER_URL`: Baron OIDC issuer URL +- `OIDC_CLIENT_ID`: server-side-app client id +- `OIDC_CLIENT_SECRET`: server-side-app client secret +- `OIDC_REDIRECT_URI`: callback URL +- `OIDC_CLIENT_AUTH_METHOD`: 기본값 `client_secret_basic`, 필요 시 `client_secret_post` +- `BARON_API_BASE_URL`: Baron backend/public gateway URL +- `BARON_BACKCHANNEL_JWKS_URL`: Baron Back-Channel Logout JWKS URL +- `BARON_SESSION_VALIDATION_ENABLED`: `false`로 두면 Baron 세션 재검증을 끄고, access token 만료 후 refresh token 갱신으로 세션 종료를 확인합니다. 기본값은 `true`입니다. + +## 라우트 + +```text +GET / +GET /login +GET /callback +GET /profile +GET /logout +POST /backchannel-logout +``` + +## 동작 방식 + +1. `/login`에서 state/nonce를 만들고 Baron authorize endpoint로 이동 +2. `/callback`에서 authorization code를 token으로 교환 +3. ID Token의 `sid/sub`를 현재 RP 세션 ID와 매핑 +4. `BARON_SESSION_VALIDATION_ENABLED=true`이면 요청마다 Baron `GET /api/v1/user/me`를 호출해 세션을 재검증 +5. `BARON_SESSION_VALIDATION_ENABLED=false`이면 access token 만료 후 Hydra token endpoint로 refresh token 갱신을 시도 +6. `invalid_grant`가 오면 로컬 세션을 파기 +7. Baron이 `/backchannel-logout`으로 `logout_token` 전송 시에도 세션을 즉시 파기 + +## 테스트 포인트 + +정상 동작 시 아래 로그 흐름이 보여야 합니다. + +```text +[로그인 시작] +[콜백] Authorization Code -> Token 교환 성공 +[세션 매핑] 등록 완료 +[백채널 로그아웃] 요청 수신 +[백채널 로그아웃] 토큰 검증 성공 +[백채널 로그아웃] 세션 파기 완료 +[백채널 로그아웃] 처리 완료 +[프로필] 비로그인 상태로 접근하여 루트로 이동 +``` diff --git a/docs/관리페이지 md 파일/Back-Channel Logout.md b/docs/관리페이지 md 파일/Back-Channel Logout.md new file mode 100644 index 0000000..17c6882 --- /dev/null +++ b/docs/관리페이지 md 파일/Back-Channel Logout.md @@ -0,0 +1,119 @@ +# Back-Channel Logout 처리 시퀀스 + +이 문서는 `baron-sso-server-side-demo`가 Baron SSO로부터 `POST /backchannel-logout` 요청을 받았을 때 어떤 순서로 동작하는지 정리합니다. + +## 개요 + +이 데모 앱은 Baron SSO가 전송한 `logout_token`을 수신하면, 다음 순서로 처리합니다. + +1. 요청 본문에서 `logout_token`을 읽습니다. +2. Baron이 서명한 토큰인지 JWKS로 검증합니다. +3. `sid` 또는 `sub`를 기준으로 로컬 세션을 찾습니다. +4. `express-session` 저장소에서 해당 세션을 삭제합니다. +5. 세션 매핑을 제거하고 `200` 응답을 반환합니다. + +## 시퀀스 다이어그램 + +```mermaid +sequenceDiagram + autonumber + participant Baron as Baron SSO + participant RP as baron-sso-server-side-demo + participant JWKS as Baron Back-Channel JWKS + participant Store as express-session Store + + Baron->>RP: POST /backchannel-logout\nlogout_token= + RP->>RP: logout_token 추출 + RP->>JWKS: JWKS 조회 후 서명 검증 + JWKS-->>RP: public key + RP->>RP: iss / aud / events / nonce / jti 검증 + RP->>RP: sid 또는 sub로 세션 매핑 조회 + RP->>Store: sessionStore.destroy(sessionId) + Store-->>RP: 삭제 완료 + RP->>RP: 세션 매핑 제거 + RP-->>Baron: 200 OK\n{ success: true } +``` + +## 실제 구현 위치 + +- 요청 수신 및 처리 엔드포인트: [`backchannel-logout.js`](../backchannel-logout.js) +- 라우트 등록과 세션 매핑: [`app.js`](../app.js) +- 동작 설명: [`README.md`](../README.md) + +## 동작 상세 + +### 1. 요청 수신 + +`app.js`에서 아래 라우트를 등록합니다. + +```javascript +app.post( + '/backchannel-logout', + backchannelLogoutManager.handleBackchannelLogout, +); +``` + +이 요청은 `application/x-www-form-urlencoded` 형식으로 전달되며, 본문에는 `logout_token`이 포함됩니다. + +### 2. 토큰 추출 + +`backchannel-logout.js`는 `req.body.logout_token`을 읽어서 빈 값인지 확인합니다. + +- 값이 없으면 `400 Bad Request` +- 값이 있으면 다음 단계로 진행 + +### 3. JWT 검증 + +데모 앱은 Baron의 백채널 JWKS를 사용해 `logout_token`을 검증합니다. + +검증 항목은 다음과 같습니다. + +- 서명 검증 +- `iss` 일치 +- `aud`에 현재 RP `clientId` 포함 +- `nonce` 미포함 +- `events`에 back-channel logout 이벤트 포함 +- `sid` 또는 `sub` 존재 +- `jti` 존재 및 재사용 방지 + +### 4. 세션 탐색 + +로그인 성공 시 저장한 `sid` / `sub` 매핑을 이용해 대상 세션을 찾습니다. + +우선순위는 다음과 같습니다. + +1. `sid`로 탐색 +2. `sid` 매칭이 없으면 `sub`로 fallback + +### 5. 세션 파기 + +매칭된 세션 ID가 있으면 `express-session` 저장소에서 직접 삭제합니다. + +```javascript +sessionStore.destroy(sessionId, callback); +``` + +삭제 후에는 세션 ID와 `sid` / `sub` 매핑도 함께 제거합니다. + +### 6. 응답 반환 + +정상 처리되면 `200 OK`와 함께 아래 응답을 반환합니다. + +```json +{ + "success": true, + "destroyedSessionCount": 1 +} +``` + +## 운영 관점 메모 + +- 이 데모는 백채널 로그아웃 외에도, 각 요청마다 Baron 세션을 재검증하는 경로를 별도로 가집니다. +- `BARON_SESSION_VALIDATION_ENABLED=false`로 두면 재검증을 끄고 백채널 로그아웃만 확인할 수 있습니다. +- Baron이 데모 앱에 직접 접근할 수 있어야 백채널 로그아웃이 성공합니다. + +## 관련 파일 + +- [`backchannel-logout.js`](../backchannel-logout.js) +- [`app.js`](../app.js) +- [`README.md`](../README.md) diff --git a/docs/관리페이지 md 파일/CODE_OF_CONDUCT.md b/docs/관리페이지 md 파일/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..288a83a --- /dev/null +++ b/docs/관리페이지 md 파일/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[dl_oss_dev@linecorp.com](mailto:dl_oss_dev@linecorp.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/관리페이지 md 파일/CONTRIBUTING.md b/docs/관리페이지 md 파일/CONTRIBUTING.md new file mode 100644 index 0000000..92cf253 --- /dev/null +++ b/docs/관리페이지 md 파일/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# How to contribute to ABC User Feedback + +First of all, thank you so much for taking your time to contribute! ABC User Feedback is not very different from any other open source projects. It will be fantastic if you help us by doing any of the following: + +- File an issue in [the issue tracker](https://github.com/line/abc-user-feedback/issues) + to report bugs and propose new features and improvements. +- Ask a question using [the issue tracker](https://github.com/line/abc-user-feedback/issues). +- Contribute your work by sending [a pull request](https://github.com/line/abc-user-feedback/pulls). +- You should make your pull request base branch as `dev` not `main`. After merging to `dev` branch, we test the pull request in our separate environment and if there is no issue, it would be merged to `main` and released. + +## Contributor license agreement + +If you are sending a pull request and it's a non-trivial change beyond fixing +typos, please make sure to sign the [ICLA (Individual Contributor License Agreement)](https://cla-assistant.io/line/abc-user-feedback). +Please [contact us](mailto:dl_oss_dev@linecorp.com) if you need the CCLA (Corporate Contributor License Agreement). + +## Code of conduct + +We expect contributors to follow [our code of conduct](./CODE_OF_CONDUCT.md). diff --git a/docs/관리페이지 md 파일/ERD.md b/docs/관리페이지 md 파일/ERD.md new file mode 100644 index 0000000..af1031c --- /dev/null +++ b/docs/관리페이지 md 파일/ERD.md @@ -0,0 +1,17 @@ +```mermaid +erDiagram + software_apps ||--o{ workspaces : "defines" + service_types ||--o{ workspaces : "defines" + users ||--o{ user_workspace_access : "has" + workspaces ||--o{ user_workspace_access : "accessed_by" + workspaces ||--o{ support_tickets : "contains" + users ||--o{ support_tickets : "requester" + support_tickets ||--o{ ticket_comments : "has" + support_tickets ||--o{ attachments : "has" + support_tickets ||--o{ request_approvals : "needs" + support_tickets ||--o{ asset_allocations : "requests" + support_tickets ||--o{ vehicle_schedules : "schedules" + support_tickets ||--o{ remote_support : "needs" + assets ||--o{ asset_allocations : "allocated" + assets ||--o{ vehicle_schedules : "scheduled" + workspaces ||--o{ faqs : "provides" \ No newline at end of file diff --git a/docs/관리페이지 md 파일/ERD_SSO.md b/docs/관리페이지 md 파일/ERD_SSO.md new file mode 100644 index 0000000..cabdcda --- /dev/null +++ b/docs/관리페이지 md 파일/ERD_SSO.md @@ -0,0 +1,211 @@ +```mermaid +erDiagram + software_apps ||--o{ workspaces : defines + service_types ||--o{ workspaces : defines + workspaces ||--o{ user_workspace_access : accessed_by + workspaces ||--o{ support_tickets : contains + workspaces ||--o{ attachments : stores + workspaces ||--o{ faqs : provides + support_tickets ||--o{ ticket_comments : has + support_tickets ||--o{ attachments : has + support_tickets ||--o{ request_approvals : needs + support_tickets ||--o{ asset_allocations : requests + support_tickets ||--o{ vehicle_schedules : schedules + support_tickets ||--o{ remote_support : needs + support_tickets ||--o{ notification_logs : notifies + assets ||--o{ asset_allocations : allocated + assets ||--o{ vehicle_schedules : scheduled + support_status_codes ||--o{ support_tickets : statuses + support_category_codes ||--o{ support_tickets : categorizes + remote_support_status_codes ||--o{ remote_support : statuses + + software_apps { + int id PK + string app_code UK + string app_name UK + string description + boolean is_active + datetime created_at + } + + service_types { + int id PK + string service_code UK + string service_name UK + string description + boolean is_active + datetime created_at + } + + workspaces { + int id PK + string workspace_type + int software_app_id FK + int service_type_id FK + string workspace_code UK + string workspace_name + boolean is_active + datetime created_at + } + + user_workspace_access { + int id PK + string user_id + string tenant_id + int workspace_id FK + string workspace_role + boolean can_read + boolean can_write + boolean can_manage + boolean can_approve + string page_scope + datetime created_at + } + + user_notification_profiles { + int id PK + string user_id + string tenant_id + string user_type + string phone_number + string naverworks_user_key + boolean sms_opt_in + datetime created_at + } + + support_status_codes { + string code PK + string name + int sort_order + } + + support_category_codes { + string code PK + string name + int sort_order + } + + support_tickets { + int id PK + int workspace_id FK + string requester_id + string requester_tenant_id + string ticket_type + string title + string content + string category_code FK + string status_code FK + boolean is_secret + datetime requested_start_at + datetime requested_end_at + string priority + datetime created_at + datetime updated_at + } + + ticket_comments { + int id PK + int ticket_id FK + string author_id + string author_tenant_id + string content + datetime created_at + } + + attachments { + int id PK + string parent_type + int parent_id + int workspace_id FK + string file_name + string file_path + int file_size + datetime created_at + } + + request_approvals { + int id PK + int ticket_id FK + string approver_id + string approver_tenant_id + string approval_status + string comment + datetime approved_at + datetime created_at + } + + assets { + int id PK + string asset_code UK + string asset_name + string asset_type + int quantity + boolean is_active + string location + datetime created_at + } + + asset_allocations { + int id PK + int ticket_id FK + int asset_id FK + string assignee_id + string assignee_tenant_id + string allocation_status + datetime loaned_at + datetime due_at + datetime returned_at + datetime created_at + } + + vehicle_schedules { + int id PK + int ticket_id FK + int asset_id FK + datetime departure_at + datetime arrival_at + string destination + string driver_name + datetime created_at + } + + remote_support_status_codes { + string code PK + string name + int sort_order + } + + remote_support { + int id PK + int ticket_id FK + string status_code FK + string support_engineer_id + string support_engineer_tenant_id + datetime scheduled_time + datetime created_at + } + + faqs { + int id PK + int workspace_id FK + string title + string content + boolean is_active + datetime created_at + } + + notification_logs { + int id PK + int ticket_id FK + string recipient_id + string recipient_tenant_id + string channel + string target_address + string delivery_status + string fallback_channel + string error_message + datetime sent_at + } + + user_notification_profiles ||..o{ notification_logs : receives +``` \ No newline at end of file diff --git a/docs/관리페이지 md 파일/GITEA_VARIABLES.md b/docs/관리페이지 md 파일/GITEA_VARIABLES.md new file mode 100644 index 0000000..3806858 --- /dev/null +++ b/docs/관리페이지 md 파일/GITEA_VARIABLES.md @@ -0,0 +1,210 @@ +# Gitea 스테이징 등록 변수 + +BARON User Feedback와 secretary-api를 `172.16.10.175:3030`으로 배포할 때 Gitea에 등록할 환경변수와 Secret 정리입니다. + +## 1. 스테이징 기본값 + +### Variables + +| 이름 | 등록값 | 용도 | +| ---------------------------- | ----------------------------------------------- | ----------------------------------------------- | +| `APP_ENV` | `staging` | 실행 환경 | +| `WEB_PORT` | `3030` | 외부 웹 포트 | +| `API_PORT` | `4000` | Docker 내부 API 포트 (host에 공개하지 않음) | +| `SECRETARY_API_PORT` | `8010` | Docker 내부 FastAPI 포트 (host에 공개하지 않음) | +| `MYSQL_PORT` | 등록 불필요 | prod Compose는 DB host port를 공개하지 않음 | +| `NEXT_PUBLIC_API_BASE_URL` | `http://172.16.10.175:3030` | Web reverse proxy를 통한 브라우저 API 주소 | +| `SUPPORT_API_ALLOWED_ORIGINS` | `http://10.13.10.4:8864` | 외부 작성 페이지에서 `/api/support/*` 호출을 허용할 origin | +| `ADMIN_WEB_URL` | `http://172.16.10.175:3030` | 관리자 웹 주소 및 OAuth 기준 주소 | +| `BASE_URL` | `http://172.16.10.175:3030` | Web reverse proxy를 통한 API 기준 주소 | +| `SMTP_ENABLED` | `false` (현재 SSO 전용) 또는 `true` (SMTP 사용) | 이메일 기능 활성화 여부 | +| `SMTP_HOST` | 사내 SMTP 호스트 | 메일 서버 | +| `SMTP_PORT` | `25` 또는 사내 SMTP 포트 | 메일 서버 포트 | +| `SMTP_SENDER` | 사내 발신 이메일 | 메일 발신자 | +| `SMTP_TLS` | `false` 또는 `true` | SMTP TLS 사용 여부 | +| `SMTP_CIPHER_SPEC` | 사내 SMTP 정책값 | TLS cipher 설정 | +| `SMTP_OPPORTUNISTIC_TLS` | `false` 또는 `true` | SMTP opportunistic TLS | +| `ACCESS_TOKEN_EXPIRED_TIME` | `10m` | Access Token 만료시간 | +| `REFRESH_TOKEN_EXPIRED_TIME` | `1h` | Refresh Token 만료시간 | +| `AUTO_MIGRATION` | `true` | API 시작 시 마이그레이션 | +| `OPENSEARCH_USE` | `false` | OpenSearch 사용 여부 | +| `OPENSEARCH_NODE` | 빈 값 | OpenSearch 미사용 | +| `OPENSEARCH_USERNAME` | 빈 값 | OpenSearch 미사용 | +| `OPENSEARCH_PASSWORD` | 빈 값 | OpenSearch 미사용 | + +> 스테이징 DB는 외부 host port를 사용하지 않습니다. API와 Secretary는 모두 `mysql:3306`의 `userfeedback` 스키마로 접근합니다. 외부 DB 접속이 필요하면 SSH 터널을 사용합니다. +> | `SSO_ISSUER` | `https://sso.hmac.kr/oidc` | secretary-api용 SSO issuer | +> | `SSO_CLIENT_ID` | `838cd69d-e722-41da-9f79-b3c42a509ef2` | BARON-SSO Client ID | + +`INTERNAL_API_BASE_URL`, `SUPPORT_API_BASE_URL`, `ABC_API_BASE_URL`는 Docker 내부 주소이므로 Gitea에 별도 등록하지 않습니다. + +```text +INTERNAL_API_BASE_URL=http://api:4000 +SUPPORT_API_BASE_URL=http://secretary-api:8010 +ABC_API_BASE_URL=http://api:4000 +``` + +다음 값도 Compose에 고정되어 있으므로 Gitea에 별도 등록하지 않습니다. + +| 이름 | 고정값 | +| ------------------- | -------------------------------------------------------------------------------- | +| `APP_NAME` | `secretary-api` | +| `APP_HOST` | `0.0.0.0` | +| `APP_PORT` | `8010` | +| `UPLOAD_ROOT_DIR` | `/app/uploads` | +| `MYSQL_PRIMARY_URL` | `mysql://userfeedback:userfeedback@mysql:3306/userfeedback` | +| `DATABASE_URL` | `mysql+pymysql://userfeedback:userfeedback@mysql:3306/userfeedback` | + +다음은 선택 기능을 사용할 때만 추가합니다. + +| 이름 | 등록값 | +| ------------------------------------ | --------------------------- | +| `MYSQL_SECONDARY_URLS` | 보조 MySQL URL JSON 배열 | +| `AUTO_FEEDBACK_DELETION_ENABLED` | `false` | +| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | 자동 삭제 사용 시 보존 일수 | + +## 2. Gitea Secrets + +### API 및 관리자 인증 + +| 이름 | 등록값 | +| ---------------------------------- | -------------------------------------------------------- | +| `JWT_SECRET` | 긴 무작위 문자열 | +| `MASTER_API_KEY` | ABC 전체 API 관리용 무작위 키 | +| `INITIAL_SUPER_ADMIN_PHONE_NUMBER` | 선택값: 초기 SUPER 관리자 자동 지정용 BARON-SSO 전화번호 | +| `ADMIN_CANDIDATE_EMAILS` | 관리자 후보 이메일 목록을 쉼표로 연결 | + +예시: + +```text +ADMIN_CANDIDATE_EMAILS=admin1@example.com,admin2@example.com +``` + +현재 로컬에 등록된 후보 이메일은 다음과 같습니다. 스테이징에서도 동일하게 사용할 때만 등록합니다. + +```text +cyhan@samaneng.com,hsmoon@hanmaceng.co.kr,hikim2@samaneng.com,thlee3@samaneng.com +``` + +### SMTP + +| 이름 | 등록값 | +| --------------- | ------------- | +| `SMTP_USERNAME` | SMTP 계정 | +| `SMTP_PASSWORD` | SMTP 비밀번호 | + +SMTP를 인증 없이 사용하면 두 값은 빈 값으로 둡니다. + +### BARON-SSO + +| 이름 | 등록값 | +| ------------------- | ----------------------- | +| `SSO_CLIENT_SECRET` | BARON-SSO Client Secret | + +BARON-SSO에 다음 Redirect URI도 등록해야 합니다. + +```text +http://172.16.10.175:3030/api/auth/baron-sso/callback +``` + +## 3. ABC API Key와 workspace 매핑 + +Gitea에는 ABC API Key와 내부 매핑 조회용 `MASTER_API_KEY`를 Secret으로 등록합니다. 프로젝트·채널 ID는 Gitea 변수나 수동 SQL로 관리하지 않고, 로그인 시 API가 ABC DB의 프로젝트·채널을 workspace code 기준으로 조회하여 `userfeedback.workspace_channel_mappings`에 자동 등록·갱신합니다. + +```text +SECRETARY_ABC_API_KEY=<스테이징 ABC API Key> +MASTER_API_KEY= +``` + +자동 매핑 대상은 다음 조건을 만족해야 합니다. + +- workspace code와 동일한 이름의 ABC 프로젝트가 정확히 1개일 것 +- 해당 프로젝트의 채널이 정확히 1개이거나, workspace code/프로젝트 이름과 일치하는 채널이 정확히 1개일 것 +- 조건을 만족하면 기존 매핑은 보완하고, 매핑 행이 없으면 새로 INSERT할 것 + +현재 매핑 확인: + +```sql +SELECT + w.id, + w.workspace_code, + w.workspace_name, + m.id AS mapping_id, + m.abc_project_id, + m.abc_channel_id, + m.is_active +FROM workspaces w +LEFT JOIN workspace_channel_mappings m + ON m.workspace_id = w.id +WHERE w.is_active = 1 +ORDER BY w.id, m.id; +``` + +배포 후 사용자가 로그아웃/로그인하거나 `/api/access/me`를 호출하면 EGBIM, TOVA 등의 누락 매핑이 자동으로 채워집니다. 프로젝트·채널 이름이 여러 개로 모호하면 잘못된 연결을 막기 위해 자동 등록하지 않으므로, 이 경우 ABC DB의 이름을 확인해야 합니다. + +## 4. DB 접속값 + +Docker 내부 서비스 간 접속값은 다음과 같습니다. + +```text +# ABC 내장 DB +MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@mysql:3306/userfeedback + +# secretary-api (ABC 단일 DB) +DATABASE_URL=mysql+pymysql://userfeedback:userfeedback@mysql:3306/userfeedback +``` + +현재 `docker-compose.prod.yml`에는 DB 계정과 비밀번호가 직접 작성되어 있습니다. + +```text +ABC DB: userfeedback / userfeedback +Secretary 보조 테이블도 위 userfeedback 스키마에 함께 저장 +``` + +운영 배포 전에는 다음 값을 Gitea Secret으로 분리하고 Compose에서 참조하도록 변경하는 것을 권장합니다. + +```text +MYSQL_ROOT_PASSWORD +MYSQL_PASSWORD +SECRETARY_MYSQL_ROOT_PASSWORD +SECRETARY_MYSQL_PASSWORD +``` + +## 5. CI/CD 배포용 변수 + +Gitea Actions에서 SSH 배포 방식을 사용할 경우 다음 값을 등록합니다. + +### Variables + +| 이름 | 등록값 | +| ---------------------- | ----------------------------------------------------------------- | +| `STAGING_HOST` | `172.16.10.175` (미등록 시 workflow 기본값 사용) | +| `STAGING_PORT` | `22` (미등록 시 workflow가 기본값으로 사용) | +| `STAGING_APP_DIR` | `/home/user/baron_qa` (미등록 시 workflow 기본값 사용) | +| `STAGING_COMPOSE_FILE` | `docker/docker-compose.prod.yml` (미등록 시 workflow 기본값 사용) | +| `STAGING_WEB_PORT` | `3030` | + +### Secrets + +| 이름 | 등록값 | +| ------------------------- | -------------------------------- | +| `STAGING_USER` | 스테이징 서버 SSH 사용자 | +| `STAGING_SSH_PRIVATE_KEY` | 배포용 SSH private key | +| `STAGING_SSH_KNOWN_HOSTS` | 스테이징 서버의 `known_hosts` 값 | + +Docker Registry를 사용하는 경우 추가로 등록합니다. + +```text +REGISTRY_URL +REGISTRY_USERNAME +REGISTRY_PASSWORD +``` + +## 6. 등록 전 확인사항 + +- 현재 스테이징은 `SMTP_ENABLED=false`로 등록하면 SMTP 없이 BARON-SSO 로그인과 권한 분기만 검증할 수 있습니다. 추후 SMTP 정보를 확보하면 `true`로 변경합니다. +- Gitea Secret에는 API Key, `MASTER_API_KEY`, JWT Secret, SMTP 비밀번호, SSO Client Secret을 등록합니다. +- ABC 프로젝트·채널 매핑은 로그인 시 ABC DB와 자동 동기화되며, 이름이 모호한 경우에만 ABC DB의 프로젝트·채널 이름을 확인합니다. +- `INITIAL_SUPER_ADMIN_PHONE_NUMBER`는 선택값입니다. 등록하면 해당 BARON-SSO 전화번호 사용자를 초기 SUPER 관리자로 자동 승격하며, 미등록 시에도 API는 정상 기동합니다. +- 외부 공개 포트는 Web `3030` 하나이며, Web이 Docker 내부 `api:4000`과 `secretary-api:8010`으로 reverse proxy합니다. +- 로컬 Compose에 존재하는 API Key와 DB 비밀번호를 스테이징에 재사용하지 말고 스테이징용 Secret을 별도로 발급합니다. diff --git a/docs/관리페이지 md 파일/GUIDE.md b/docs/관리페이지 md 파일/GUIDE.md new file mode 100644 index 0000000..e50574d --- /dev/null +++ b/docs/관리페이지 md 파일/GUIDE.md @@ -0,0 +1,217 @@ +# ABC User Feedback Integration Guide + +## Image Storage Integration + +ABC User Feedback supports the integration of image storage solutions to handle images submitted as part of user feedback. We currently support AWS S3 and S3-compatible storage services. + +### Uploading Images + +There are two methods for uploading images associated with feedback: + +1. **Multipart Upload API**: This method requires setting up the [image configuration](#S3-configuration). Once configured, you can use the multipart upload API to securely upload images directly to your storage service. + +2. **Feedback Creation API with Image URLs**: Alternatively, users can submit feedback with image URLs. This method does not require the image configuration setup; however, the image URLs must come from the whitelisted domains. + +**Note**: For detailed instructions on using these methods, please refer to the API documentation. You can see the documentation by accessing to `{API server host}/docs` or `{API server host}/docs/redoc`. + +### S3 Configuration + +To enable image uploads directly to the server, you must configure the image storage settings. The service uses the following configuration parameters and you can set them in the setting menu. + +- `accessKeyId`: Your storage service access key ID. +- `secretAccessKey`: Your storage service secret access key. +- `endpoint`: The endpoint URL for the storage service. +- `region`: The region your storage service is located in. +- `bucket`: The name of the bucket where images will be stored. +- `enablePresignedUrlDownload`: Enable the setting to enhance download security by using the pre-signed URL feature supported by AWS S3. + +Depending on your use case and the desired level of access, you may need to adjust the permissions of your S3 bucket. If your application requires that the images be publicly accessible, configure your S3 bucket's policy to allow public reads. + +### Domain Whitelist + +Users can specify a whitelist of domains for image URLs. This ensures that only images from trusted sources are accepted and managed by User Feedback API server. + +**Note**: The domain whitelist is enforced at the time of posting feedback with images. This means that validation against the whitelist occurs only during the submission of new feedback. Once an image URL has been uploaded to the database and accepted, it will be accessible through the web admin interface regardless of its current status on the whitelist. It is important to ensure that image URLs are from trusted sources before they are uploaded, as subsequent changes to the whitelist will not retroactively affect previously stored image URLs. + +## Webhook Feature + +### Introduction to Webhooks + +Webhooks in ABC User Feedback provide a powerful way to integrate with external services. They allow you to receive real-time notifications when specific events occur within the application, such as new feedback submissions or issue updates. + +Furthermore, you can combine webhooks with ABC User Feedback API to make more powerful features such as translation, sentiment analysis or whatever you want. Just make a webhook listener and build your own script and send the result to ABC User Feedback by API. + +### Setting Up Webhooks + +To set up webhooks in ABC User Feedback: + +1. Navigate to the project settings where you want to enable webhooks. +2. Add a new webhook by providing the URL endpoint that ABC User Feedback will send the data to when events occur. +3. Turn on the events you wish to subscribe to. +4. Save the webhook configuration. + +Ensure that the endpoint you provide is secure and can accept POST requests with a JSON payload. + +### Event Types and Request Bodies + +ABC User Feedback's webhook supports the following event types, each with its own specific payload structure: + +| No. | Title | Description | +|-----|--------------------|------------------------------------| +| 1 | FEEDBACK_CREATION | When the new Feedback is created. | +| 2 | ISSUE_ADDITION | When an Issue is added to a feedback. | +| 3 | ISSUE_CREATION | When the new Issue is created. | +| 4 | ISSUE_STATUS_CHANGE | When the Issue status is changed. | + + +#### 1. FEEDBACK_CREATION + +This event is triggered when a new piece of feedback is created. + +**Payload Structure:** + +```json +{ + "event": "FEEDBACK_CREATION", + "data": { + "feedback": { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "issues": [ + { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "name": "issue name", + "description": "issue description", + "status": "INIT", + "externalIssueId": "123", + "feedbackCount": 1 + } + ] + }, + "channel": { + "id": 1, + "name": "channel name" + }, + "project": { + "id": 1, + "name": "project name" + } + } +} +``` + +#### 2. ISSUE_ADDITION + +This event is triggered when an issue is added to an existing piece of feedback. + +**Payload Structure:** + +```json +{ + "event": "ISSUE_ADDITION", + "data": { + "feedback": { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "issues": [ + { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "name": "issue name", + "description": "issue description", + "status": "INIT", + "externalIssueId": "123", + "feedbackCount": 1 + } + ] + }, + "channel": { + "id": 1, + "name": "channel name" + }, + "project": { + "id": 1, + "name": "project name" + }, + "addedIssue": { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "name": "issue name", + "description": "issue description", + "status": "INIT", + "externalIssueId": "123", + "feedbackCount": 1 + } + } +} +``` + +#### 3. ISSUE_CREATION + +This event is triggered when a new issue is created within a project. + +**Payload Structure:** + +```json +{ + "event": "ISSUE_CREATION", + "data": { + "issue": { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "name": "issue name", + "description": "issue description", + "status": "INIT", + "externalIssueId": "123", + "feedbackCount": 1 + }, + "project": { + "id": 1, + "name": "project name" + } + } +} +``` + +#### 4. ISSUE_STATUS_CHANGE + +This event is triggered when the status of an issue is updated. + +**Payload Structure:** + +```json +{ + "event": "ISSUE_STATUS_CHANGE", + "data": { + "issue": { + "id": 1, + "createdAt": "2023-04-02T15:30:00Z", + "updatedAt": "2023-04-02T15:30:00Z", + "name": "issue name", + "description": "issue description", + "status": "ON_REVIEW", + "externalIssueId": "123", + "feedbackCount": 1 + }, + "project": { + "id": 1, + "name": "project name" + }, + "previousStatus": "INIT" + } +} +``` + +### Handling Webhooks + +Upon receiving a webhook payload, your endpoint should: + +Parse the JSON payload. +Take appropriate action based on the event type and data received. diff --git a/docs/관리페이지 md 파일/README.md b/docs/관리페이지 md 파일/README.md new file mode 100644 index 0000000..43b123a --- /dev/null +++ b/docs/관리페이지 md 파일/README.md @@ -0,0 +1,365 @@ +# ABC User Feedback + +## 프로젝트 개요 + +회사 내 여러 RP(Request Point, 요청 접수 서비스)의 Q&A를 한곳에서 확인·처리하는 **Q&A 통합 관리 플랫폼**. + +주요 기능: + +- 사용자의 Q&A 작성 +- 여러 프로젝트·RP의 Q&A를 모아 보는 관리자 콘솔 +- 피드백, 댓글, 내부 메모, 첨부파일 관리 +- 이슈 등록 및 Gitea 이슈 연결 +- BARON-SSO 기반 로그인·권한 관리 +- 관리자 업무를 보조하는 Secretary API + +## 통합 관리가 필요한 이유 + +### 기존 운영 방식 + +사내 S/W 홈페이지, 인트라넷, 총무 관련 요청 게시판 등 RP별로 회원 관리와 Q&A 관리가 분리된 구조. + +### 기존 문제 + +- 서비스마다 별도 계정·권한 관리 필요 +- 관리자가 여러 페이지를 각각 확인해야 함 +- 답변, 담당자, 첨부파일, 이슈 처리 이력이 서비스별로 분산 +- 서비스마다 상태·관리 방식이 달라 통합 현황과 통계 확인 어려움 + +### 통합 필요성 + +BARON-SSO 도입으로 여러 서비스의 회원 인증과 사용자 정보를 하나의 기준으로 통합 가능. + +회원 관리가 통합된 시점에서 Q&A 관리도 통합 필요. 여러 RP에서 들어오는 Q&A를 하나의 관리자 콘솔에서 관리하고, 답변·담당자·이슈·처리 상태를 동일한 기준으로 연결. + +### 기대 효과 + +- 관리자의 여러 페이지 이동 감소 +- 문의 누락 방지 및 처리 현황 실시간 확인 +- Q&A부터 이슈 처리까지 단일 업무 흐름으로 관리 +- RP별 권한은 유지하면서 전체 현황은 통합 조회 +- 사용자 인증과 문의 처리 경험의 일관성 확보 + +## 핵심 용어 + +| 용어 | 의미 | +| ------------- | ----------------------------------------------------------------------------------------------------------------------- | +| RP | 사내 S/W 홈페이지, 인트라넷, 총무 요청 게시판 등 Q&A를 접수하는 각각의 서비스·요청 창구 | +| Q&A | 사용자가 작성 페이지에서 직접 등록한 질문·요청·불편 사항 | +| 피드백 | Q&A가 관리자 콘솔에 도착한 뒤 관리 대상이 된 항목. 같은 요청을 사용자 관점에서는 Q&A, 관리자 관점에서는 피드백으로 구분 | +| 관리자 콘솔 | 여러 프로젝트·RP의 피드백을 확인하고 처리하는 화면. 통합 관리와 프로젝트별 관리 제공 | +| 프로젝트 | 서비스 또는 업무 단위를 구분하는 관리 단위 | +| 채널 | 프로젝트 안에서 Q&A를 접수하는 세부 창구. 입력 항목과 화면 설정을 채널별로 지정 | +| 이슈 | 피드백 처리를 위해 등록하는 작업 항목. 하나의 피드백에 여러 이슈 연결 가능 | +| Gitea 이슈 | 개발자가 소스 코드 수정·개발 작업을 진행하는 외부 이슈. 관리자 콘솔의 이슈와 연결 가능 | +| BARON-SSO | 여러 사내 서비스의 로그인과 사용자 정보를 통합하는 인증 시스템 | +| Secretary API | BARON-SSO 접근 권한, 워크스페이스, 담당자·알림 등 관리 업무를 보조하는 API | + +## 프로젝트 목표와 처리 흐름 + +여러 RP의 Q&A 작성부터 관리자 처리와 개발 이슈 연결까지 하나의 기준으로 연결. + +```text +사용자가 RP에서 Q&A 작성 + ↓ +ABC API에 Q&A 저장 + ↓ +관리자 콘솔에 피드백으로 표시 + ↓ +관리자가 확인·답변·담당자 지정 + ↓ +필요하면 관리자 이슈 또는 Gitea 이슈로 연결 + ↓ +처리 결과를 기록하고 Q&A 작성자에게 안내 +``` + +핵심 목표: + +1. BARON-SSO 기반 로그인·사용자 식별 통합 +2. 여러 RP의 Q&A를 관리자 콘솔에서 통합 관리 +3. 피드백, 댓글, 내부 메모, 첨부파일, 이슈 처리 이력 연결 +4. 프로젝트별 권한 유지와 전체 현황 통합 조회 +5. ABC API/ABC DB를 피드백 원본으로 사용하고 보조 시스템에는 식별자·업무 메타데이터만 저장 + +이 문서는 저장소의 시작점입니다. 상세 설계와 작업 이력은 [`docs/`](./docs/) 아래에 보존하고, 여기에는 실제 실행·검증·배포 순서와 문서 선택 기준을 정리합니다. + +## 1. 빠른 시작 + +### 로컬 실행 + +필요 조건: + +- Node.js `24.14.1` (`.nvmrc`)과 `pnpm@10.32.1` +- Docker 및 Docker Compose +- `apps/secretary-api/.venv`와 Secretary API 의존성 + +권장 실행 명령은 루트의 `start-local.sh`입니다. 이 스크립트가 로컬 MySQL·Redis·smtp4dev를 올린 뒤 Web, ABC API, Secretary API를 함께 실행합니다. 현재 셸의 Node 버전이 다르면 설치된 nvm에서 `.nvmrc` 버전을 자동 선택합니다. + +```bash +pnpm install +./start-local.sh +``` + +접속 주소: + +- Web: +- ABC API: +- ABC Swagger: +- ABC 관리자 Swagger: +- Secretary API: +- Secretary Swagger: +- SMTP 테스트함: +- Redis: `127.0.0.1:16379` (로컬 비밀번호 필요) + +이미 인프라를 실행한 상태에서 개발 서버만 시작하려면 다음을 사용할 수 있습니다. + +```bash +pnpm dev:local +``` + +포트 `3100`, `4000`, `8010`을 사용하는 프로세스가 있으면 스크립트가 임의로 종료하지 않고 중단합니다. 먼저 사용 중인 프로세스를 확인한 뒤 종료하고 다시 실행합니다. + +```bash +lsof -nP -iTCP:3100 -sTCP:LISTEN +lsof -nP -iTCP:4000 -sTCP:LISTEN +lsof -nP -iTCP:8010 -sTCP:LISTEN +``` + +### 로컬 검증 + +```bash +pnpm lint +pnpm typecheck +pnpm build +``` + +E2E는 테스트용 데이터베이스를 초기화할 수 있으므로 로컬 개발 데이터와 분리된 환경에서 실행합니다. + +```bash +pnpm test:e2e +``` + +## 2. 서비스 구조 + +```text +브라우저 + │ + ▼ +apps/web Next.js 화면 및 내부 BFF + ├─▶ apps/api NestJS, ABC 피드백·이슈 API + │ └─▶ ABC MySQL + └─▶ apps/secretary-api FastAPI, SSO 접근·워크스페이스·업무 보조 API + └─▶ ABC MySQL (지원 보조 테이블 포함) + +외부 연동: BARON-SSO · Gitea · SMTP · R2/S3 호환 스토리지 · OpenSearch(선택) +``` + +| 구성요소 | 책임 | 기본 포트 | +| -------------------- | --------------------------------------------------- | --------------------------: | +| `apps/web` | 관리자 콘솔, 사용자 피드백 화면, Secretary 내부 BFF | `3100` 로컬 / `3030` Docker | +| `apps/api` | ABC 프로젝트·채널·피드백·이슈·댓글·필드·통계 API | `4000` | +| `apps/secretary-api` | SSO 접근권한, 워크스페이스, 지원 업무 API | `8010` | +| ABC MySQL | 피드백·이슈·사용자·권한·Secretary 보조 데이터 | `13306` 로컬 | + +## 3. 데이터와 SSOT 원칙 + +피드백의 원본은 관리자 화면이 아니라 ABC API/ABC DB입니다. 관리자 콘솔과 사용자 페이지 모두 같은 ABC API를 조회·작성·수정·삭제에 사용해야 합니다. + +ABC가 보유하는 원본: + +- 피드백 ID, 제목, 내용, 생성일, 수정일 +- 작성자 이름·이메일·부서·연락처와 SSO 식별자 +- 카테고리, 비밀글 여부, IP 주소, MAC 주소 +- 중요도와 피드백 처리 상태 +- 첨부파일, 댓글, 내부 메모 +- ABC 이슈 연결 관계 + +Secretary가 보유하는 데이터: + +- SSO 접근권한과 역할 +- 프로젝트/워크스페이스 접근 설정 +- 업무용 담당자·승인·알림·매핑 메타데이터 + +Secretary DB에 피드백 제목·내용·상태를 별도로 복제하지 않습니다. 연결이 필요하면 `abc_feedback_id` 같은 식별자만 보조 데이터로 사용합니다. 피드백 상태와 이슈 상태도 서로 독립적으로 관리합니다. + +## 4. 인증과 권한 흐름 + +1. 사용자가 BARON-SSO OAuth/OIDC 로그인 화면으로 이동합니다. +2. Web의 callback이 인증 코드를 ABC API에 전달합니다. +3. ABC API가 SSO 프로필을 조회하고 사용자·테넌트 정보를 반영한 JWT를 발급합니다. +4. Web은 세션 쿠키로 JWT를 유지합니다. +5. 접근 가능한 프로젝트와 워크스페이스를 조회한 뒤 관리자 통합 대시보드 또는 사용자 피드백 화면으로 분기합니다. + +관련 구현 위치: + +- SSO callback: [`apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts`](./apps/web/src/features/auth/sign-in-with-oauth/lib/use-oauth-callback.ts) +- API 인증: [`apps/api/src/domains/admin/auth/`](./apps/api/src/domains/admin/auth/) +- Web 접근 제어: [`apps/web/src/proxy.ts`](./apps/web/src/proxy.ts) +- Secretary 접근 정보: [`apps/secretary-api/`](./apps/secretary-api/) + +SSO 프로필의 `name`, `email`, `phones`, `employee_id`, `status` 등의 필드를 사용할 때는 BARON-SSO의 `profile` scope와 실제 callback 응답을 함께 확인합니다. 토큰, client secret, API key는 코드나 README에 기록하지 않습니다. + +## 5. 주요 기능 사용 가이드 + +### 사용자 피드백 + +사용자는 프로젝트/채널의 필드 설정에 따라 피드백을 등록합니다. 현재 확장 필드에는 구분, 중요도, IP, MAC 주소 등이 포함될 수 있으며, 비밀글과 첨부파일을 지원합니다. 사용자 목록은 제목·내용·작성자 검색, 작성자 본인 글 필터, 구분 필터, 정렬, 10건 단위 페이지 이동을 제공합니다. + +### 관리자 피드백 처리 + +관리자는 피드백 상태, 중요도, 피드백 담당자를 관리하고 댓글과 내부 메모를 구분해 기록합니다. 내부 메모는 관리자에게만 노출되며 일반 댓글과 동시에 저장되지 않아야 합니다. 피드백을 이슈에 연결해도 피드백 상태와 이슈 상태는 각각 별도로 처리합니다. + +### 이슈와 Gitea + +이슈를 연결한 뒤 이슈 담당자를 지정하고 Gitea 이슈를 생성·연결·동기화할 수 있습니다. 하나의 이슈에 여러 피드백을 연결할 수 있으며, 연결된 피드백 목록과 Gitea 상태를 확인합니다. + +### 통합 관리자 대시보드 + +관리자 사용자는 로그인 후 통합 관리 화면으로 이동. 피드백 처리 탭에서는 담당자 지정·댓글·피드백 상태를, 이슈 처리 탭에서는 Gitea 연결·이슈 상태·연결 피드백을 프로젝트별로 처리. 대시보드 집계와 Todo는 권한이 있는 프로젝트만 대상. + +## 6. API와 Swagger + +ABC API 문서는 공개 연동 API와 관리자 API를 분리합니다. + +| 구분 | 로컬 경로 | 주요 인증 | +| ------------------- | ------------------ | ------------------------------------- | +| 공개 API Swagger | `/docs` | API Key가 필요한 엔드포인트는 API Key | +| 관리자 API Swagger | `/admin-docs` | JWT 및 권한 | +| 공개 OpenAPI JSON | `/docs-json` | 환경 설정에 따름 | +| 관리자 OpenAPI JSON | `/admin-docs-json` | 환경 설정에 따름 | +| Secretary Swagger | `:8010/docs` | FastAPI 설정에 따름 | + +로컬에서는 API 포트로 직접 확인합니다. Docker 스테이징에서는 API 컨테이너 포트를 외부에 별도로 열지 않고 Web reverse proxy를 통해 다음 경로를 사용합니다. 최신 Web 이미지가 배포된 뒤 동작합니다. + +- +- +- +- Secretary OpenAPI JSON: + +API 패키징 시에는 화면용 Next.js `/api/support/*` BFF를 외부 계약으로 사용하지 않고, NestJS 공개/관리자 API와 Secretary API를 공식 경계로 취급합니다. API 버전, 페이지네이션, 동적 필드, 오류 형식, 댓글 공개 범위는 [`docs/api-packaging-tasks.md`](./docs/api-packaging-tasks.md)를 기준으로 확정합니다. + +## 7. 환경변수와 비밀값 + +환경별 값을 코드와 문서에 하드코딩하지 않습니다. 스테이징에서는 Gitea Actions 변수/Secret 또는 서버의 실제 환경 주입 방식을 사용합니다. + +주요 변수 그룹: + +- 실행: `APP_ENV`, `WEB_PORT`, `APP_PORT`, `SECRETARY_API_PORT` +- Web/API 주소: `NEXT_PUBLIC_API_BASE_URL`, `ADMIN_WEB_URL`, `BASE_URL` +- 인증: `JWT_SECRET`, `MASTER_API_KEY`, `SSO_ISSUER`, `SSO_CLIENT_ID`, `SSO_CLIENT_SECRET` +- 연동: `GITEA_API_URL`, `GITEA_API_TOKEN`, SMTP 변수, `SECRETARY_ABC_API_KEY` +- 운영: `AUTO_MIGRATION`, `OPENSEARCH_USE` 및 OpenSearch 변수 + +Docker 내부 서비스는 `api:4000`, `secretary-api:8010`, `mysql:3306`으로 통신합니다. ABC와 Secretary는 같은 `userfeedback` 스키마를 사용합니다. 브라우저에 노출되는 외부 포트는 Web 포트 하나로 제한합니다. 환경변수 전체 목록과 Gitea 등록 규칙은 [`GITEA_VARIABLES.md`](./GITEA_VARIABLES.md)를 확인합니다. + +R2/S3 호환 첨부파일은 버킷과 endpoint를 환경 또는 관리자 설정으로 주입합니다. Access Key, Secret Key, API Token은 절대 커밋하지 않습니다. 이미지·첨부파일 저장 원칙은 [`GUIDE.md`](./GUIDE.md)를 참고합니다. + +## 8. 스테이징 배포 + +배포 전 로컬: + +```bash +git status +pnpm lint +pnpm typecheck +pnpm build +``` + +스테이징 서버에서는 실제 환경 파일 또는 배포 시스템이 제공하는 환경을 사용해 Compose 설정을 먼저 검증합니다. + +```bash +cd ~/baron_qa +docker compose --env-file <실제-환경파일> \ + -f docker/docker-compose.prod.yml config + +docker compose --env-file <실제-환경파일> \ + -f docker/docker-compose.prod.yml up -d --build + +docker compose --env-file <실제-환경파일> \ + -f docker/docker-compose.prod.yml ps +``` + +기존 데이터가 있는 서버에서 `docker compose down -v`를 실행하지 않습니다. `-v`는 DB 볼륨을 삭제할 수 있습니다. 최초 배포나 스키마 변경 시 백업, `AUTO_MIGRATION`, 로그, health check를 확인합니다. + +상세 순서는 [`STAGING_DEPLOYMENT_CHECKLIST.md`](./STAGING_DEPLOYMENT_CHECKLIST.md)를 따릅니다. + +## 9. 문제 해결 순서 + +1. 브라우저 주소가 `localhost`/`127.0.0.1`인지, 스테이징 도메인인지 확인합니다. +2. Network에서 요청 URL과 응답 코드, 특히 `401`, `404`, `500`을 확인합니다. +3. Web이 API를 `localhost`가 아닌 Docker 내부 `api:4000`으로 호출하는지 확인합니다. +4. SSO callback URL의 host, scheme, path가 BARON-SSO 등록값과 같은지 확인합니다. +5. 프로젝트/채널/워크스페이스 이름과 ABC 매핑을 확인합니다. +6. 서버 로그를 확인합니다. + +```bash +docker compose --env-file <실제-환경파일> \ + -f docker/docker-compose.prod.yml logs --tail=200 web api secretary-api +``` + +데이터가 보이지 않는다고 DB를 초기화하거나 볼륨을 삭제하지 않습니다. 먼저 API 응답의 프로젝트 ID, 채널 ID, 테넌트, 권한을 확인합니다. + +## 10. 상세 문서 목차 + +### 운영팀 사용 안내 + +- [`관리페이지-운영팀-사용가이드.md`](./docs/관리페이지-운영팀-사용가이드.md): 비개발자 운영팀을 위한 프로젝트·채널·피드백·이슈 처리 가이드 +- [`관리페이지-개념설명.md`](./docs/관리페이지-개념설명.md): Q&A·피드백·프로젝트·채널·이슈의 관계와 주요 용어 설명 + +### 운영·배포 + +- [`STAGING_DEPLOYMENT_CHECKLIST.md`](./STAGING_DEPLOYMENT_CHECKLIST.md): 정적 검사, 환경변수, Compose 배포, 배포 후 검증 +- [`GITEA_VARIABLES.md`](./GITEA_VARIABLES.md): Gitea Actions 변수/Secret 및 스테이징 매핑 +- [`GUIDE.md`](./GUIDE.md): S3/S3 호환 스토리지와 Webhook 관련 기본 가이드 + +### API·SSOT·기능 작업 + +- [`api-packaging-tasks.md`](./docs/api-packaging-tasks.md): 외부 패키지용 API 경계, Swagger, DTO, 버전 정책 +- [`ssot-feedback-rearchitecture-tasks.md`](./docs/ssot-feedback-rearchitecture-tasks.md): ABC DB를 피드백 SSOT로 통일한 단계별 작업 +- [`qna-platform-prototype-2-feedback-tasks.md`](./docs/qna-platform-prototype-2-feedback-tasks.md): Q&A 관리자 콘솔 기능과 API 작업 이력 + +### 통합 아키텍처 + +- [`architecture_secretary_sso_components_v2.md`](./docs/architecture_secretary_sso_components_v2.md): 현재 통합 구조를 설명하는 우선 참고 문서 +- [`architecture_secretary_sso_role_access.md`](./docs/architecture_secretary_sso_role_access.md): 역할, 테넌트, 로그인 후 접근 분기 +- [`architecture_secretary_sso_user_scenarios.md`](./docs/architecture_secretary_sso_user_scenarios.md): 사용자 유형별 업무 시나리오 +- [`architecture.md`](./docs/architecture.md): 초기 통합 지원 플랫폼 설계 +- [`architecture_secretary_sso_components.md`](./docs/architecture_secretary_sso_components.md): 통합 컴포넌트 설계 초안 + +### SSO + +- [`BARON-SSO server-side-app guide.md`](<./docs/BARON-SSO server-side-app guide.md>): 서버 애플리케이션의 BARON-SSO 연동 예시 +- [`Back-Channel Logout.md`](<./docs/Back-Channel Logout.md>): Back-Channel Logout 처리 순서와 구현 위치 +- [`architecture_secretary_sso_setup_tasks_v2.md`](./docs/architecture_secretary_sso_setup_tasks_v2.md): 최신 SSO 연계 셋업 및 남은 작업 + +### 데이터 이관·통합 대시보드 + +- [`egbim_to_secretary_staging_migration_tasks.md`](./docs/egbim_to_secretary_staging_migration_tasks.md): EGBIM/Secretary 데이터 이관 범위와 검증 +- [`multi-project-admin-dashboard-design.md`](./docs/multi-project-admin-dashboard-design.md): 다중 프로젝트 관리자 통합 대시보드 설계 + +### 이전 버전·참고 문서 + +다음 문서는 앞선 설계 버전 또는 중복된 셋업 문서입니다. 현재 구현과 충돌할 경우 코드와 위의 v2/SSOT 문서를 우선합니다. + +- [`architecture_secretary.md`](./docs/architecture_secretary.md) +- [`architecture_secretary_sso.md`](./docs/architecture_secretary_sso.md) +- [`architecture_secretary_sso_setup_tasks.md`](./docs/architecture_secretary_sso_setup_tasks.md) + +문서의 작업 완료 표시는 당시 기준의 기록입니다. 배포 전에는 반드시 현재 코드, Compose 파일, 환경변수와 함께 대조합니다. + +## 11. 저장소 구조 + +```text +apps/ + api/ NestJS ABC API + web/ Next.js Web/BFF + secretary-api/ FastAPI Secretary API + e2e/ Playwright E2E + docs/ Docusaurus 기반 일반 제품 문서 +docs/ 프로젝트 설계·작업·운영 보조 문서 +docker/ 로컬/스테이징 Compose 및 Dockerfile +scripts/ 로컬 실행·이관·개발 보조 스크립트 +uploads/ 로컬 첨부파일 마운트 경로 +``` + +기여 방법은 [`CONTRIBUTING.md`](./CONTRIBUTING.md), 라이선스는 [`LICENSE`](./LICENSE)를 확인합니다. diff --git a/docs/관리페이지 md 파일/STAGING_DEPLOYMENT_CHECKLIST.md b/docs/관리페이지 md 파일/STAGING_DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..cfb6d63 --- /dev/null +++ b/docs/관리페이지 md 파일/STAGING_DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,261 @@ +# 스테이징 배포 체크리스트 + +현재 로컬 변경사항을 스테이징 서버에 배포한 뒤 테스트하기 위한 절차입니다. + +## 1. 배포 전 로컬 확인 + +E2E 테스트는 배포 후 진행합니다. 배포 전에는 정적 검사와 빌드만 확인합니다. + +```bash +git status +git diff --stat +pnpm lint +pnpm typecheck +pnpm build +``` + +빌드가 성공하면 변경사항을 커밋하고 스테이징 배포 브랜치에 푸시합니다. + +```bash +git add <배포할 파일 목록> +git commit -m "Update feedback workflow" +git push origin +``` + +`.env` 파일, 비밀번호, API 키, E2E 결과 파일은 커밋하지 않습니다. + +## 2. Gitea 및 스테이징 환경변수 준비 + +Gitea Actions를 사용할 때 다음 값을 등록합니다. 프로젝트·채널 UUID는 런타임에 ABC 내부 API로 조회하므로 별도 환경변수로 등록하지 않습니다. + +### Gitea Variables + +- `STAGING_HOST=feedback.hmac.kr` +- `ABC_DB_USER=userfeedback`: workflow에서 컨테이너의 `MYSQL_USER`로 매핑 +- `MYSQL_BIND_ADDRESS=10.13.10.4`, `MYSQL_PORT=13306`: MySQL을 스테이징 내부 인터페이스에만 게시 +- `NEXT_PUBLIC_API_BASE_URL=https://feedback.hmac.kr/api` +- `ADMIN_WEB_URL=https://feedback.hmac.kr` +- `BASE_URL=https://feedback.hmac.kr` +- `SSO_ISSUER`, `SSO_CLIENT_ID` +- `ALLOW_OAUTH_EMAIL_LINKING=true`: 초기 관리자 이메일 계정을 최초 SSO 로그인 시 동일 계정으로 연결 +- `ADMIN_CANDIDATE_TENANT_ID` +- `INITIAL_SUPER_ADMIN_PHONE_NUMBER` +- `DEFAULT_SUPPORT_WORKSPACE_CODE` +- workflow에 이미 정의된 SMTP, OpenSearch, Naver Works 등의 일반 설정값 + +### Gitea Secrets + +- `ABC_DB_ROOT_PASSWORD`: workflow에서 컨테이너의 `MYSQL_ROOT_PASSWORD`로 매핑 +- `ABC_DB_PASSWORD`: workflow에서 컨테이너의 `MYSQL_PASSWORD`와 DB URL로 매핑 +- `REDIS_PASSWORD` +- `JWT_SECRET` +- `MASTER_API_KEY`: Web의 동적 workspace 매핑과 내부 서비스 인증에 공통 사용 +- `SECRETARY_ABC_API_KEY`: ABC 공개 프로젝트 API 호출용 키 +- `SSO_CLIENT_SECRET` +- workflow에 이미 정의된 SMTP, Object Storage, 외부 이슈, Naver Works 등의 비밀값 + +`MYSQL_PRIMARY_URL`, `SECRETARY_DATABASE_URL`, `REDIS_URL`은 workflow가 위 계정·비밀번호를 URL-encoding하여 컨테이너 내부 주소로 생성하므로 Gitea에 따로 등록하지 않습니다. 다음 과거 고정 매핑값도 더 이상 사용하지 않습니다. + +- `SUPPORT_*_PROJECT_ID` +- `SUPPORT_*_CHANNEL_ID` +- `ABC_PROJECT_ID` +- `ABC_CHANNEL_ID` +- `SECRETARY_ABC_PROJECT_CONFIG` + +DB 초기화 후에는 새 프로젝트·채널 생성까지 완료한 다음 ABC 화면에서 API Key를 재발급하고 `SECRETARY_ABC_API_KEY` Secret 값을 갱신해야 합니다. 프로젝트·채널 UUID 자체는 갱신할 필요가 없으며 Web은 최대 30초 안에 새 매핑을 조회합니다. + +수동 Compose 배포를 사용할 때는 스테이징 서버에 `.env.staging` 파일을 준비하고 다음 항목도 설정합니다. + +- `NEXT_PUBLIC_API_BASE_URL` +- `SUPPORT_API_ALLOWED_ORIGINS` (예: `http://10.13.10.4:8864`) +- `ADMIN_WEB_URL` +- `BASE_URL` +- `JWT_SECRET` +- `MASTER_API_KEY` +- `SECRETARY_ABC_API_KEY` +- `MYSQL_DATABASE=userfeedback` +- `MYSQL_USER`, `MYSQL_ROOT_PASSWORD`, `MYSQL_PASSWORD` +- `MYSQL_BIND_ADDRESS=10.13.10.4`, `MYSQL_PORT=13306` +- `MYSQL_PRIMARY_URL` (`mysql:3306/userfeedback`) +- `SECRETARY_DATABASE_URL` (`mysql:3306/userfeedback`) +- `REDIS_PASSWORD`, `REDIS_URL` (`redis:6379/0`) +- SSO issuer, client ID, client secret +- Gitea API URL 및 token +- SMTP 설정 +- OpenSearch 설정 +- 최초 배포 시 `AUTO_MIGRATION=true` + +웹 환경변수는 Docker 이미지 빌드 시 `apps/web/.env.build`에서 사용됩니다. + +```text +apps/web/.env.build +``` + +스테이징 API 주소가 설정되어 있어야 하며, `localhost` 또는 `127.0.0.1` 주소가 남아 있으면 안 됩니다. + +## 3. 스테이징 서버에서 코드 갱신 + +```bash +git pull origin +``` + +배포 전에 Compose 설정을 검증합니다. + +```bash +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml config +``` + +## 4. 스테이징 배포 + +```bash +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml \ + up -d --build +``` + +컨테이너 상태와 로그를 확인합니다. + +```bash +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml ps + +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml \ + logs --tail=200 api secretary-api web +``` + +MySQL 포트가 정상적으로 게시되었는지는 다음 출력에서 +`10.13.10.4:13306->3306/tcp`로 확인합니다. + +MySQL은 API 통신용 내부 `backend` 네트워크와 호스트 관리 접속용 +`database_access` 네트워크에 동시에 연결됩니다. Redis와 API의 내부 네트워크는 +외부에 공개되지 않습니다. + +```bash +docker ps \ + --filter label=com.docker.compose.project=abc-user-feedback-deploy \ + --filter label=com.docker.compose.service=mysql \ + --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' +``` + +기존 스테이징 데이터가 있다면 다음 명령은 실행하지 않습니다. + +```bash +docker compose down -v +``` + +`-v` 옵션은 데이터베이스 볼륨을 삭제할 수 있습니다. 기존 데이터가 있다면 배포 전에 DB 백업도 진행합니다. + +## 5. 배포 후 테스트 순서 + +### 로그인 및 권한 + +- 관리자 SSO 로그인 및 callback URL 확인 +- DB 초기화 직후 `/api/admin/tenants`가 `useOAuth: true`를 반환하고 `clientSecret`은 `********`로 마스킹되는지 확인 +- DB 초기 설정에서 만든 관리자와 SSO 이메일이 같을 때 중복 사용자 오류 없이 기존 계정으로 연결되는지 확인 +- 관리자 계정이 콘솔/대시보드로 이동하는지 확인 +- 일반 사용자 계정이 피드백 리스트로 이동하는지 확인 +- 브라우저 콘솔과 Network에 401/500 오류가 없는지 확인 + +### 피드백 목록 + +- 리스트에 10개씩 표시되는지 확인 +- 첫 페이지, 다음 페이지, 마지막 페이지 이동 확인 +- ID, 제목, 내용, 이슈, 상태, Created, Updated 순서 확인 +- 정렬 기능 확인 +- 리스트/칸반 전환 확인 +- 상태별 조회 확인 +- 중요도 표시와 중요도별 배경색 확인 + +### 피드백 상태 및 상세 + +- 피드백 상태가 신규·접수·진행중·완료·보류 5단계로 표시되는지 확인 +- 칸반에서 드래그하여 상태 변경되는지 확인 +- 드래그 중 카드가 마우스를 따라가는 모션 확인 +- 상태별 배경색 확인 +- 피드백 상태와 이슈 상태가 독립적으로 변경되는지 확인 +- IP 주소와 MAC 주소가 상세 페이지에 표시되는지 확인 +- 수정 페이지에서 IP 주소와 MAC 주소를 수정할 수 있는지 확인 +- 입력 항목의 툴팁 안내문구 확인 +- 프로젝트·채널 UUID를 환경변수에 등록하지 않아도 workspace 목록·상세가 열리는지 확인 +- 프로젝트/채널 재생성 후 30초 이내 새 UUID 매핑으로 조회되는지 확인 + +### 내부 메모 및 댓글 + +- 내부 메모가 관리자 화면에서만 표시되는지 확인 +- 내부 메모 저장 시 외부 댓글이 동시에 등록되지 않는지 확인 +- 외부 댓글 등록 시 내부 메모가 중복 생성되지 않는지 확인 +- 동일한 `Idempotency-Key`로 댓글 요청을 재시도해 댓글이 한 건만 생성되는지 확인 + +### 피드백 자동 처리 + +- 관리자 최초 열람 시 신규에서 접수로 자동 변경되는지 확인 +- 관리자 공개 댓글 등록 시 진행중으로 자동 변경되는지 확인 +- 처리 완료 안내 댓글 등록 후 완료 확인 대기 표시가 나타나는지 확인 +- Q&A 작성자 완료 확인 시 완료로 변경되는지 확인 +- 완료 확인 전 재문의 시 대기 정보가 해제되고 진행중이 유지되는지 확인 +- 완료 상태 재문의 시 자동 재오픈되지 않고 관리자 검토 대상으로 남는지 확인 +- 자동 처리 실패 시 상세 화면에 원인과 수동 처리 안내가 표시되는지 확인 + +### 이슈 및 Gitea 연동 + +- 피드백에서 이슈를 연결할 수 있는지 확인 +- 하나의 이슈에 여러 피드백을 연결할 수 있는지 확인 +- 추가로 연결한 피드백도 Gitea 이슈에 반영되는지 확인 +- 이슈 연결 후 이슈 관리자를 지정할 수 있는지 확인 +- 이슈 관리자 목록이 설정 페이지의 등록 목록과 일치하는지 확인 +- 이슈 상태는 Gitea 처리 기준으로 독립적으로 변경되는지 확인 +- 이슈 연결 요청을 재시도해 동일 연결과 통계가 중복 반영되지 않는지 확인 + +### 외부 Q&A 작성 API + +- 외부 작성 페이지가 브라우저에 ABC API Key를 노출하지 않고 Web 서버 경유로 호출하는지 확인 +- 외부 Q&A 생성, 댓글, 첨부 댓글, 완료 확인 API의 인증·작성자 식별값을 확인 +- 외부 댓글 요청에 `Idempotency-Key`를 넣어 네트워크 재시도 중복이 방지되는지 확인 + +## 6. 문제 발생 시 확인할 로그 + +```bash +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml \ + logs -f api + +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml \ + logs -f secretary-api + +docker compose \ + --env-file .env.staging \ + -f docker/docker-compose.prod.yml \ + logs -f web +``` + +브라우저에서는 다음을 함께 확인합니다. + +- Console 오류 +- Network 요청 URL +- 응답 상태 코드 +- 401 Unauthorized 여부 +- API callback 및 redirect 주소 +- Gitea, SMTP, OpenSearch 연결 오류 + +## 7. 롤백 시 주의사항 + +- 이전 정상 커밋 또는 태그를 유지합니다. +- DB 백업을 먼저 확보합니다. +- 애플리케이션 롤백 시에도 DB 볼륨은 삭제하지 않습니다. +- 마이그레이션이 포함된 배포는 애플리케이션만 무조건 이전 버전으로 되돌리지 않습니다. + +현재 로컬 서버는 `pnpm dev:local`로 실행 중이며, 스테이징 배포와는 별개입니다. 스테이징 배포는 로컬 작업 디렉터리에서 직접 실행하지 말고, 커밋 후 스테이징 서버에서 코드를 갱신한 뒤 진행합니다. + +배포 기준 Compose 파일: + +- `docker/docker-compose.prod.yml` +- `docker/web.dockerfile` diff --git a/docs/관리페이지 md 파일/abc-single-database-migration-tasks.md b/docs/관리페이지 md 파일/abc-single-database-migration-tasks.md new file mode 100644 index 0000000..307d971 --- /dev/null +++ b/docs/관리페이지 md 파일/abc-single-database-migration-tasks.md @@ -0,0 +1,86 @@ +# ABC DB 단일화 전환 작업 목록 + +- 작성일: 2026-09-07 +- 결정: 스테이징 및 운영에서 피드백·사용자·권한 데이터를 ABC `userfeedback` MySQL 하나로 관리한다. +- 목표: `baron_support` DB와 `mysql-secretary`를 제거하고, Secretary 서비스가 필요로 하는 보조 테이블도 ABC `userfeedback` DB 안에서 관리한다. + +## 1. 확인된 현재 상태 + +| 항목 | ABC DB (`userfeedback`, 13306) | 구축 DB (`baron_support`, 13308) | +| ------------ | ---------------------------------- | -------------------------------------------------- | +| OAuth 사용자 | `users`에 저장됨 | 스테이징 `support_users` 0건 | +| 피드백 | `feedbacks` 등 ABC 테이블에 저장됨 | `support_tickets` 등은 스테이징 실사용 데이터 없음 | +| 배포 구성 | Nest API가 직접 사용 | `secretary-api`가 연결하도록 구성만 남아 있음 | + +현재 배포 구성에 정의된 영속 DB는 위 두 MySQL뿐이다. `MYSQL_SECONDARY_URLS`는 기능으로는 지원하지만 스테이징 Compose 환경에는 설정되지 않았다. + +## 2. 전환 원칙 + +1. ABC `feedbacks.id`를 피드백의 유일한 식별자로 유지한다. +2. 사용자 인증과 관리자 권한은 ABC `users`, `roles`, `members`를 원본으로 사용한다. +3. Secretary 서비스는 유지하되 `DATABASE_URL`을 ABC `userfeedback`으로 전환한다. 지원 전용 테이블은 같은 물리 DB 안에 둔다. +4. 기존 외부 작성페이지는 계속 ABC API로 등록할 수 있어야 하며, 통합관리 화면은 같은 ABC 피드백을 조회·처리한다. +5. 이관할 구축 DB 실데이터가 없으므로 `baron_support` 데이터 이관은 수행하지 않는다. 삭제 전 최종 백업만 남긴다. + +## 3. 작업 범위 + +### 3.1 사용자·권한 + +- [x] `support_users` 등 Secretary 보조 테이블을 `userfeedback` 스키마에 생성할 migration을 준비하고 임시 MySQL에서 검증했다. OAuth 원본 사용자는 계속 ABC `users`로 유지한다. +- [x] Secretary 권한 조회는 내부 API를 통해 ABC `users.type` 및 `members → roles → projects`를 원본으로 사용하도록 보완했다. 기존 Secretary 역할은 ABC API 일시 장애 시에만 호환용으로 유지한다. +- [ ] 로그인 직후 이동, 프로젝트 가드, 통합 대시보드의 권한 규칙이 ABC 사용자·프로젝트 정보와 같은 DB 안에서 정상 동작하는지 확인한다. + +### 3.2 피드백 및 지원 화면 + +- [x] `support_tickets` 등 지원 화면 보조 테이블을 ABC 피드백과 동일한 `userfeedback` DB에 생성할 수 있음을 임시 MySQL 마이그레이션으로 확인했다. +- [x] ABC `attachments`와 이름이 충돌하는 Secretary 테이블을 `support_attachments`로 분리한다. +- [ ] 피드백 상태, 담당자, 이슈 연결, 댓글, 내부 메모, 첨부파일이 동일 DB에서 정상 동작하는지 검증한다. + +### 3.3 외부 작성페이지 연동 + +- [ ] `10.13.10.4:8864` EGBIM_DEMO 작성페이지의 생성·목록 API가 ABC 프로젝트/채널을 일관되게 사용함을 확인한다. +- [ ] `feedback.hmac.kr` 통합관리 페이지와 외부 작성페이지가 같은 ABC feedback ID를 조회하는지 검증한다. +- [ ] `/api/support/*`를 외부 작성페이지의 공개 계약으로 사용하지 않는다. 외부 연동은 ABC 공개 API 또는 확정된 별도 API 계약을 사용한다. + +### 3.4 배포·인프라 정리 + +- [x] 스테이징 Compose의 Secretary `DATABASE_URL`을 `mysql:3306/userfeedback`으로 전환한다. +- [x] 스테이징 Compose에서 `mysql-secretary`, `baron_support` volume, 외부 13308 포트를 제거한다. +- [x] 기본·local·infra·apps·prod Compose에서 `mysql-secretary`를 제거하고 Secretary DB 연결을 `userfeedback`으로 통일했다. E2E는 원래 Secretary DB를 사용하지 않는다. +- [x] 현재 실행에 사용되는 Secretary migration/config, 로컬 시작 스크립트, 배포 문서를 ABC 단일 DB 기준으로 갱신했다. 과거 이관 SQL·과거 설계 문서는 역사 기록으로 유지한다. +- [x] Gitea 변수 및 스테이징 배포 workflow를 ABC 단일 DB 구성으로 갱신했다. +- [x] 프로젝트·채널 UUID 고정 환경변수를 제거하고 `MASTER_API_KEY`로 ABC 내부 API를 조회하는 동적 workspace 매핑으로 전환했다. +- [ ] 실제 DB 삭제 전 `baron_support` 백업 및 컨테이너/볼륨 참조가 없는지 재확인한다. (스테이징 배포·기능 확인 후 수행) + +## 4. 단계별 검증 기준 + +1. OAuth 로그인 후 `userfeedback.users`만 생성·갱신되고 권한 화면 및 통합관리 화면이 정상으로 열린다. +2. EGBIM_DEMO에서 피드백 작성 후 ABC `feedbacks`에 1건 생성되고, 통합관리 및 작성페이지 목록에 동일 ID·제목·작성자가 표시된다. +3. 관리자 상태 변경, 담당자 지정, 이슈 연결, 댓글·첨부파일의 생성/조회/수정/삭제를 ABC 데이터만으로 확인한다. +4. [x] `mysql-secretary` 없이 `secretary-api`, Web, API가 기동되고 Secretary migration이 ABC `userfeedback`에 적용됨을 임시 MySQL에서 확인한다. +5. 삭제 직전 스테이징에서 `baron_support` 연결 시도 로그가 없는 것을 확인한 뒤 DB/볼륨을 제거한다. + +## 5. 배포 전 필수 미완료 항목 + +아래 항목은 코드를 작성했다고 자동으로 완료되지 않는다. 실제 스테이징 배포와 사용자 동작 확인이 필요하므로 체크를 유지한다. + +- [ ] 스테이징 배포 후 Secretary migration이 `userfeedback`의 `alembic_version`에 `0021_ticket_idempotency`로 기록됐는지 확인한다. +- [ ] 스테이징 `userfeedback`에 `support_attachments`와 Secretary 보조 테이블이 생성됐고, 기존 ABC `attachments`가 유지됐는지 확인한다. +- [ ] OAuth 로그인, 통합관리 권한, EGBIM_DEMO 작성/목록/상세/상태변경을 실제 스테이징에서 확인한다. +- [ ] `SUPER` 사용자와 EGBIM_DEMO의 `PROJECT_MANAGER`/`Admin` 사용자가 `/api/tickets/{id}/internal-memos` 등 관리자 API를 403 없이 호출하는지 확인한다. +- [ ] `mysql-secretary` 컨테이너가 기동되지 않고, `baron_support` 연결 오류가 없는지 로그로 확인한다. +- [ ] `baron_support` 전체 백업 후 DB/볼륨을 삭제한다. 이 작업은 별도 운영 승인 후에만 수행한다. + +### 로컬 적용 기록 (2026-09-07) + +- [x] 기존 로컬 `mysql-secretary` 컨테이너를 제거했다. 기존 볼륨은 삭제하지 않았다. +- [x] Secretary를 ABC MySQL에 연결해 재기동했고 `/api/health`가 `200`을 반환했다. +- [x] 로컬 및 임시 빈 DB에서 `alembic_version=0021_ticket_idempotency`, `support_users`, `support_tickets`, `support_attachments` 생성을 확인했다. +- [x] 로컬에서 Secretary → ABC 내부 `identity-access` API의 서비스 키 인증·응답 및 권한 조회 캐시를 확인했다. +- [x] API/Web typecheck 및 Web lint를 통과했다. + +## 6. 위험 및 결정 필요 항목 + +- `ticket_comments`, 내부 메모, 승인 상태처럼 ABC 테이블과 1:1 대응하지 않는 Secretary 기능은 `userfeedback` DB의 지원 전용 테이블로 유지한다. +- 현재 작업 트리의 `support-abc.ts`, `support-types.ts`, workspace tickets API 변경은 진행 중인 표시번호 작업으로 보인다. 단일 DB 전환 시 덮어쓰지 않고 유지·검증한다. +- 데이터 삭제는 코드·배포 전환과 스테이징 검증이 완료된 후 별도 승인으로 수행한다. diff --git a/docs/관리페이지 md 파일/admin-console-integration-feedback-requirements-2026-09-02.md b/docs/관리페이지 md 파일/admin-console-integration-feedback-requirements-2026-09-02.md new file mode 100644 index 0000000..e0c2cb3 --- /dev/null +++ b/docs/관리페이지 md 파일/admin-console-integration-feedback-requirements-2026-09-02.md @@ -0,0 +1,766 @@ +# 관리페이지 통합 관리 피드백 정리 + +- 작성일: 2026-09-02 +- 대상: 관리페이지의 프로젝트 통합 관리페이지(`/main/overview`) +- 목적: 오늘 피드백을 기준으로 통합 관리페이지 및 관리자 설정의 수정 범위를 먼저 정의한다. +- 문서 상태: 요구사항 및 자동화 시퀀스 초안 + +## 1. 반영 범위 + +이번 범위는 다음 화면을 중심으로 한다. + +- 프로젝트 통합 관리페이지 + - 피드백 처리 탭 + - 이슈 처리 탭 + - 상단 요약 지표 + - 피드백 상세 팝업 +- 관리자 설정 + - 관리자 목록 + - 프로젝트별 기본 관리자 지정 +- 공통 상단 네비게이션 + - 프로필 메뉴 + - 언어 설정 + - 설정 메뉴 +- 프로젝트 목록 및 프로젝트 선택 영역 + +## 2. 요구사항 목록 + +### 2.1 상단 및 공통 네비게이션 + +#### 1) 관리자 Todo 부연 설명 문구 + +- 관리자 Todo 아래의 부연 설명 문구를 제거한다. +- 상태: **추후 적용 / 별도 지시 전까지 보류** +- 별도 지시가 있을 때까지 현재 문구의 적용 상태는 유지한다. + +#### 2) 상단 요약 데이터 박스 + +- 다음 항목을 제거한다. + - 이슈 연결률 + - 평균 처리 시간 +- 나머지 6개 항목은 한 줄에 표시한다. +- 6개 박스가 화면 너비를 균등하게 사용할 수 있도록 width와 grid를 조정한다. +- 현재 유지 대상 항목: + - 오늘 등록된 피드백 + - 오늘 답변 대기 + - 담당자 미지정 + - 피드백 상태 처리 필요 + - 전체 피드백 + - 답변 대기 + +#### 3) 언어 설정 위치 + +- 최상단에 별도로 노출된 언어 설정 항목을 제거한다. +- 프로필 아이콘 하위 메뉴의 옵션으로 이동한다. +- 언어 설정 항목의 아이콘은 제거한다. + +#### 4) 테넌트 설정 및 화면 색상 설정 위치 + +- 테넌트 설정을 `Setting` 메뉴 하위 항목으로 이동한다. +- 화면 색상 설정을 `Setting` 메뉴 하위 항목으로 이동한다. +- 최상단에 직접 노출된 기존 항목은 제거한다. + +#### 5) Setting 메뉴 위치 및 아이콘 + +- `Setting` 메뉴를 톱니바퀴 아이콘으로 표시한다. +- 프로필 아이콘의 오른쪽에 정렬한다. +- 기존 `Setting` 텍스트 노출 여부는 아이콘 중심으로 재검토한다. + +### 2.2 통합 관리페이지 피드백 처리 탭 + +#### 6) Like 검색 + +- 피드백 처리 탭 상단에 Like 검색 기능을 추가한다. +- 검색 입력과 검색 실행 영역은 기존 리스트 상단 필터 영역과 함께 배치한다. +- 검색 대상과 검색 방식은 현재 피드백 API의 검색 조건을 재사용할 수 있도록 설계한다. + +#### 7) 담당자 및 피드백 상태 표시 방식 + +- 리스트의 담당자와 피드백 상태 항목은 현재 값만 표시한다. +- 리스트 안에서 직접 변경하는 컨트롤은 제거한다. +- 담당자 지정 및 피드백 상태 변경은 피드백 상세 팝업 안에서만 제공한다. +- 상세 팝업에서 변경한 결과는 리스트에 즉시 반영한다. + +#### 8) 관리자 댓글 컬럼 + +- 피드백 리스트의 관리자 댓글 컬럼을 삭제한다. +- 관리자 댓글 작성 및 조회는 상세 팝업에서 처리한다. + +#### 9) 생성일 및 업데이트일 + +- 생성일 옆에 업데이트일을 추가한다. +- 업데이트일은 다음 이벤트가 발생했을 때 변경되어야 한다. + - 피드백 설정 변경 + - 관리자 댓글 등록 +- 업데이트일은 리스트와 상세 팝업에서 동일한 기준으로 표시한다. +- 관리자 댓글이 별도 테이블에 저장되는 현재 구조를 고려해, 댓글 등록 시 피드백의 최종 업데이트 시각을 갱신하거나 별도 통합 업데이트 시각을 제공해야 한다. + +### 2.3 프로젝트 통합 구조 + +#### 10) 통합 관리페이지의 프로젝트 편입 + +- 프로젝트 통합 관리페이지를 프로젝트 목록의 프로젝트 항목으로 편입한다. +- 프로젝트 목록 최상단에 고정한다. +- 통합 관리페이지의 프로젝트 번호는 `0`으로 고정한다. +- 프로젝트 번호 정렬 및 표시 로직은 `0`번 프로젝트가 항상 최상단에 오도록 조정한다. +- 추후 프로젝트 코드는 4자리 형식으로 변경될 예정이므로, 프로젝트 ID와 표시용 프로젝트 코드를 분리할 수 있도록 설계한다. +- 일반 프로젝트의 기존 이동 및 선택 동작은 유지한다. + +### 2.4 관리자 설정 및 기본 관리자 + +#### 11) 프로젝트별 기본 관리자 지정 + +- 관리자 설정에 프로젝트별 기본 관리자 지정 기능을 추가한다. +- 각 프로젝트에 기본 관리자를 지정할 수 있어야 한다. +- 관리자 설정 화면에서 현재 지정된 기본 관리자를 확인하고 변경할 수 있어야 한다. + +#### 12) 기본 관리자 자동 담당자 지정 + +- 프로젝트에 기본 관리자가 지정되어 있으면, 피드백에 별도 담당자가 없을 때 기본 관리자를 담당자로 사용한다. +- 사용자가 별도로 담당자를 지정한 경우에는 별도 지정값을 우선한다. +- 프로젝트에 기본 관리자가 없으면 현재처럼 담당자 항목을 공란으로 표시한다. +- 자동 지정 시점은 피드백 생성 시점과 조회 시점의 데이터 일관성을 고려해 결정한다. + +### 2.5 신규 피드백 표시 및 정렬 + +#### 13) 읽지 않은 피드백 New 표시 + +- 누구든지 피드백을 한 번이라도 읽으면 해당 피드백을 읽은 상태로 기록한다. +- 한 명이라도 읽은 피드백은 신규 강조 대상에서 제외한다. +- 아직 아무도 읽지 않은 피드백은 피드백 제목 옆에 `NEW`를 표시한다. +- `NEW`는 빨간색으로 강조한다. +- 피드백 상세 팝업 진입 또는 읽음 처리 API 호출 시 읽음 상태가 저장되어야 한다. +- 프로젝트 통합 관리페이지의 목록 갱신 후에도 읽음 상태가 유지되어야 한다. + +#### 14) 피드백 리스트 정렬 + +- 피드백 리스트 상단에 정렬 기능을 추가한다. +- 정렬 기준과 오름차순/내림차순을 선택할 수 있어야 한다. +- 최소 정렬 기준 후보: + - 생성일 + - 업데이트일 + - 피드백 상태 + - 담당자 + - 우선순위 +- 기본 정렬 기준과 정렬 상태 유지 범위는 구현 단계에서 확정한다. + +### 2.6 피드백 상세 팝업 개선 + +#### 16) 상세 팝업 이슈 처리 + +- 피드백 상세 팝업에서 연결된 이슈를 확인할 수 있어야 한다. +- 상세 팝업에서 기존 이슈를 피드백에 연결하거나 연결 해제할 수 있어야 한다. +- 연결된 이슈별 상태를 상세 팝업에서 변경할 수 있어야 한다. +- 이슈 연결/해제 및 상태 변경 결과는 목록에 즉시 반영되어야 한다. +- 이슈 연결/해제는 `feedback_issue_update`, 이슈 상태 변경은 `issue_update` 권한을 따른다. + +#### 17) 이슈 상태 및 피드백 상태 배치 + +- 상세 팝업의 `처리 상태` 제목과 설명 문구를 제거한다. +- `이슈 상태`와 `피드백 상태`를 한 줄의 동일한 너비(각 50%)로 배치한다. +- 상태 변경 컨트롤은 각 영역 안에서 제공한다. + +#### 18) 사용자 정보 표시 + +- 피드백 상세 팝업의 사용자 정보에서 `부서` 항목을 제외한다. +- 이름, 이메일, 전화번호 및 접속 정보 등 나머지 사용자 정보는 유지한다. + +#### 19) 댓글/내부 메모가 없는 상세 팝업 높이 + +- 최초 댓글과 내부 메모가 모두 없는 경우 빈 목록 영역 때문에 불필요한 세로 스크롤이 생기지 않아야 한다. +- 상세 팝업은 실제 콘텐츠 높이에 맞춰 표시하되, 화면 높이를 초과하는 경우에만 내부 스크롤을 제공한다. +- 관리자 댓글 목록과 댓글 작성 영역은 한 화면에서 함께 확인할 수 있도록 배치한다. + +#### 20) 상세 팝업 보조 설명 문구 + +- 첨부파일, 담당자, 내부 메모, 관리자 댓글 등 각 항목의 추가 부연 설명 문구를 제거한다. +- 항목명과 실제 데이터/입력 영역을 우선 노출해 관리자 댓글 작성 영역이 가려지지 않도록 한다. + +## 3. 현재 구현과의 연결 지점 + +현재 통합 관리페이지는 다음 구조를 사용한다. + +- 화면: `apps/web/src/pages/main/overview.tsx` +- 통합 대시보드 조회: `GET /api/admin/dashboard/overview` +- 피드백 Todo 목록: 대시보드 API의 `todos` 응답 +- 관리자/담당자 권한 조회: `secretary-api`의 `/api/access/*` +- 관리자 설정 화면: `apps/web/src/widgets/setting-menu/ui/tenant/admin-permission-setting.ui.tsx` +- 현재 리스트에서 담당자와 피드백 상태를 직접 변경하는 UI가 존재한다. +- 현재 리스트에 관리자 댓글 컬럼과 댓글 입력 흐름이 존재한다. +- 현재 피드백의 `createdAt`, `updatedAt`은 기본 피드백 데이터 기준이며 관리자 댓글 변경 시각과의 통합 여부를 별도로 보완해야 한다. + +## 4. 데이터 및 API 변경 검토사항 + +### 4.1 관리자 기본값 + +- 프로젝트와 기본 관리자 간 매핑 저장 위치를 결정한다. +- 기본 관리자를 1명으로 제한할지 여러 명 허용할지 결정한다. +- 기본 관리자 지정/변경/해제 API가 필요하다. +- 관리자 설정 응답에 프로젝트별 기본 관리자 정보를 포함해야 한다. + +### 4.2 피드백 읽음 상태 + +- 피드백별 읽음 상태 저장 테이블 또는 필드가 필요하다. +- “누구든지 한 번이라도 읽음”을 판정할 수 있어야 한다. +- 읽은 사용자, 읽은 시각을 감사 추적용으로 저장할지 결정한다. +- 목록 조회 시 `isNew` 또는 동등한 필드를 반환하는 방식을 검토한다. + +### 4.3 업데이트일 + +- 피드백 본문/설정 변경과 관리자 댓글 등록을 하나의 업데이트 시각으로 통합할지 결정한다. +- 기존 `feedback.updated_at`을 갱신하는 방식과 이벤트/이력 기반 방식 중 하나를 선택한다. +- 외부 ABC 데이터와 내부 Secretary 데이터의 업데이트 시각 기준을 구분해야 한다. + +### 4.4 검색 및 정렬 + +- Like 검색의 대상 필드를 확정한다. + - 제목 + - 내용 + - 요청자 정보 + - 카테고리 + - 기타 동적 필드 +- 검색을 서버 API 쿼리로 처리할지, 현재 조회 결과의 클라이언트 필터로 처리할지 결정한다. +- 정렬 가능한 필드와 null 값 정렬 규칙을 API에 정의한다. +- 페이지네이션 및 최대 조회 건수 제한을 정렬/검색과 함께 검토한다. + +### 4.5 통합 프로젝트 0번 + +- 프로젝트 목록 API가 가상 프로젝트 `0`을 반환할지, 프론트에서 고정 항목으로 삽입할지 결정한다. +- 통합 관리페이지를 일반 프로젝트와 동일한 타입으로 다룰지 별도 타입으로 둘지 결정한다. +- 기존 `projectId`가 필요한 API 호출과 통합 페이지용 API 호출을 분리한다. +- 향후 4자리 프로젝트 코드 변경을 고려해 `projectId`, `projectCode`, `displayOrder`를 혼용하지 않는다. + +## 5. 우선순위 제안 + +### 1순위: 목록 사용성 및 데이터 표시 + +- 2. 요약 박스 6개 한 줄 표시 +- 7. 담당자/상태 읽기 전용 표시 및 상세 팝업 제어 +- 8. 관리자 댓글 컬럼 삭제 +- 9. 업데이트일 표시 +- 14. 리스트 정렬 +- 6. Like 검색 + +### 2순위: 권한 및 자동화 + +- 11. 프로젝트별 기본 관리자 지정 +- 12. 기본 관리자 자동 담당자 지정 +- 13. 읽지 않은 피드백 `NEW` 표시 + +### 3순위: 정보 구조 및 네비게이션 + +- 3. 언어 설정 이동 +- 4. 테넌트/화면 색상 설정 이동 +- 5. Setting 아이콘 정렬 +- 10. 통합 관리페이지의 프로젝트 편입 및 프로젝트 번호 `0` + +### 보류 + +- 1. 관리자 Todo 부연 설명 문구 제거 + - 별도 지시 전까지 보류 + +## 6. 완료 기준 + +- 요구사항별 변경 범위와 API/데이터 변경점이 구현 전에 확정되어야 한다. +- 통합 관리페이지에서 피드백 검색, 정렬, 읽음 상태, 업데이트일이 동일한 기간/프로젝트 범위 기준으로 동작해야 한다. +- 담당자와 피드백 상태는 리스트에서 수정할 수 없고 상세 팝업에서만 수정할 수 있어야 한다. +- 이슈 연결/해제 및 이슈 상태 변경은 피드백 상세 팝업에서 처리할 수 있어야 한다. +- 상세 팝업은 댓글/내부 메모가 없을 때 불필요한 스크롤을 만들지 않아야 한다. +- 상세 팝업의 항목별 보조 설명 문구는 노출하지 않아야 한다. +- 관리자 댓글은 리스트 컬럼에 노출되지 않고 상세 팝업에서만 처리되어야 한다. +- 기본 관리자가 지정된 프로젝트의 미지정 피드백에는 기본 담당자가 표시되어야 한다. +- 프로젝트 통합 관리페이지는 프로젝트 목록 최상단에서 프로젝트 번호 `0`으로 식별되어야 한다. +- 1번 항목은 별도 지시 전까지 구현하지 않는다. + +## 7. 구현 전 확정이 필요한 질문 + +1. Like 검색의 대상 필드는 제목/내용만으로 할지, 요청자·카테고리까지 포함할지? +2. 프로젝트별 기본 관리자는 1명만 허용할지, 여러 명을 허용할지? +3. 기본 관리자 자동 지정은 피드백 생성 시 실제 데이터를 저장할지, 조회 시 기본값으로 표시할지? +4. 피드백을 읽은 것으로 처리하는 시점은 리스트 노출 시점인지, 제목 클릭/상세 팝업 진입 시점인지? +5. 읽음 상태를 사용자별로 저장할지, 피드백별 최초 읽음 여부만 저장할지? +6. 업데이트일을 기존 `feedback.updated_at`으로 통일할지, 댓글까지 포함한 별도 `lastActivityAt`을 둘지? +7. 리스트 기본 정렬은 최신 업데이트일순인지, 최신 생성일순인지? +8. 통합 관리페이지의 프로젝트 번호 `0`을 API 프로젝트 목록에도 포함할지, 화면에서만 가상 항목으로 처리할지? + +## 8. 피드백·이슈 자동처리 시퀀스 및 예외 정책 + +- 작성일: 2026-09-03 +- 목적: 현재 수동으로 처리하는 피드백·이슈 상태 변경 중 자동화할 수 있는 범위와 예외 상황을 공유한다. +- 용어 정의: + - `Q&A`는 사용자가 Q&A 작성 페이지에서 직접 작성한 원문이다. + - `피드백`은 작성된 Q&A가 관리페이지에 도착해 관리되는 접수 항목이다. 즉, Q&A와 피드백은 같은 내용을 가리키지만 사용되는 화면과 역할이 다르다. +- 표기 규칙: 아래 다이어그램의 `S`는 관리페이지 시스템, `A`는 관리자, `F`는 Q&A 작성자, `D`는 개발자를 의미한다. NaverWorks(SMS) 알림은 관리페이지 시스템이 직접 처리하며 별도 참여자로 표시하지 않는다. + +### 8.1 공통 상태 정의 + +관리페이지에서 관리되는 피드백과 이슈는 모두 다음 다섯 단계만 사용한다. Q&A는 작성자가 입력한 원문이므로 별도의 처리 상태를 가지지 않고, 관리페이지에 도착한 뒤 피드백으로 관리된다. + +| 상태 | 코드 예시 | 정의 | 자동 변경 여부 | +| ------ | ------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| 신규 | `NEW` | 피드백이나 이슈가 처음 만들어져 아직 누구도 처리하지 않은 상태 | 생성할 때 관리페이지 시스템이 자동으로 지정 | +| 접수 | `RECEIVED` | 관리자가 내용을 확인해 처리 대상으로 받아들인 상태 | 관리자가 피드백을 처음 열면 자동 변경. Gitea 이슈 연결 시 이슈도 자동 변경 | +| 진행중 | `IN_PROGRESS` | 관리자나 개발자가 실제로 답변·수정 작업을 진행하는 상태 | 관리자 댓글이나 이슈 연결 뒤 피드백은 자동 변경. 개발자의 이슈 변경은 수동 처리 | +| 완료 | `COMPLETED` | 처리 결과가 확정되어 추가 조치가 필요하지 않은 상태 | Q&A 작성자 확인 또는 확인 기간 만료 뒤 피드백 자동 변경. 이슈 완료는 개발자 수동 처리 | +| 보류 | `ON_HOLD` | 관리자가 판단해 처리를 잠시 멈춘 상태 | 관리자가 직접 설정하고 직접 해제 | + +#### 완료 확인을 별도 상태로 추가하지 않는 원칙 + +- 5단계 상태를 유지하기 위해 `완료 대기`라는 여섯 번째 상태값은 만들지 않는다. +- 관리자가 처리 완료 안내 댓글을 등록하면 피드백 상태는 `진행중`을 유지하고, `completion_requested=true`와 `completion_requested_at` 메타데이터를 기록한다. +- Q&A 작성자가 완료 확인을 하거나 확인 기간이 만료된 때에만 피드백 상태를 `완료`로 변경한다. +- 화면에는 `진행중` 상태와 함께 “Q&A 작성자 확인 대기” 배지를 표시할 수 있다. 이 배지는 상태값이 아니다. +- 완료 확인 대기 중 Q&A 작성자의 재문의가 발생하면 `completion_requested`를 해제하고 피드백을 `진행중`으로 유지한다. + +### 8.2 자동 변경의 기본 규칙 + +#### 피드백 + +- 생성: `신규` +- 관리자가 관리페이지의 피드백 상세 페이지를 처음 열람: 피드백 상태를 `신규 → 접수`로 자동 변경 +- 관리자가 처리 과정에 대한 댓글을 등록: 피드백 상태를 `신규/접수 → 진행중`으로 자동 변경 +- 관리자가 피드백에 이슈를 연결해 처리를 시작: 피드백 상태를 `신규/접수 → 진행중`으로 자동 변경 +- 관리자가 처리 완료 안내 댓글을 등록: `completion_requested=true` 기록 후 `진행중` 유지 +- 보류 상태에서는 위 자동 변경을 적용하지 않는다. 관리자가 먼저 수동으로 보류를 해제해야 한다. +- 완료 상태에서 단순 열람·댓글·동기화만으로 자동 재오픈하지 않는다. 동일 문제에 대한 Q&A 재문의는 관리자에게 알리고, 관리자가 `완료 → 진행중`으로 수동 변경한다. +- 완료 확인 대기 중 Q&A 작성자의 재문의는 관리자 확인 없이 피드백을 `진행중`으로 되돌리고 관리자에게 알린다. + +#### 관리자 댓글 유형 + +- 일반 관리자 댓글은 처리 과정의 안내·질문·추가 확인에 사용한다. 피드백이 `신규/접수` 상태라면 댓글 등록과 함께 `진행중`으로 자동 변경한다. +- 처리 완료를 알리는 댓글은 `completion_notice=true` 메타데이터를 가진 “처리 완료 안내 댓글”로 구분한다. +- 처리 완료 안내 댓글은 별도의 완료 처리 버튼이 아니라, 관리자 댓글 작성 영역에서 완료 안내 유형을 선택해 등록하는 방식으로 정의한다. +- 처리 완료 안내 댓글을 등록하면 Q&A 작성자에게 완료 확인을 표시하고, `completion_notice_comment_id`를 저장한다. +- 완료 확인 대기 중 일반 댓글은 대기 상태를 유지하지만, Q&A 작성자의 재문의 댓글은 완료 확인 대기를 해제한다. + +#### 이슈 + +- 이슈 생성: `신규` +- 관리자가 이슈를 처음 열람하거나 Gitea 이슈를 연결: 이슈 상태를 `신규 → 접수`로 자동 변경 +- 개발자가 Gitea 이슈를 확인한 뒤 이슈 상태를 수동 변경: `접수 → 진행중` +- 개발자가 처리를 끝낸 뒤 이슈 상태를 수동 변경: `진행중 → 완료` +- 개발자 또는 관리자가 완료된 이슈를 다시 열면 이슈를 `진행중`으로 수동 변경한다. +- 이슈의 `보류` 설정·해제는 항상 관리자 수동 처리이며, 외부 Gitea 상태 동기화가 덮어쓰지 않는다. + +### 8.3 보류 상태의 우선 규칙 + +- `보류`는 피드백과 이슈 모두 관리자만 직접 설정하고 해제할 수 있다. +- 자동화 이벤트는 현재 상태가 `보류`인 피드백·이슈의 상태를 변경하지 않는다. + - 관리자 댓글 등록 + - 이슈 연결 + - Gitea 상태 동기화 + - Q&A 작성자의 댓글 또는 재문의 + - 기간 만료 +- 보류 중 Q&A 작성자의 재문의는 댓글과 NaverWorks(SMS) 알림 이력만 기록하고, 피드백 또는 이슈 상태는 `보류`로 유지한다. +- 관리자가 보류를 해제할 때 `접수` 또는 `진행중` 중 하나를 직접 선택한다. +- 보류 중인 이슈가 포함된 피드백은 모든 이슈가 완료되어도 자동 완료 처리하지 않는다. + +### 8.4 다중 이슈 집계 규칙 + +하나의 피드백에는 이슈가 여러 개 연결될 수 있으며, 각 이슈는 서로 독립적으로 상태를 가진다. + +- 이슈가 하나라도 `신규`, `접수`, `진행중`이면 피드백은 `진행중`으로 유지한다. +- 이슈 하나가 `완료`가 되어도 다른 이슈가 미완료이면 피드백을 완료 처리하지 않는다. +- 연결된 모든 이슈가 `완료`여도 관리자가 결과를 확인하고 처리 완료 안내 댓글을 등록해야 한다. +- 관리자가 처리 완료 안내 댓글을 등록한 뒤 이슈를 추가로 연결하면 완료 확인 대기를 해제하고 피드백을 `진행중`으로 되돌린다. +- 이슈를 모두 연결 해제해도 피드백을 자동으로 완료하지 않는다. 관리자가 피드백 처리 결과를 직접 확인해야 한다. +- 이슈 중 하나라도 `보류`이면 피드백 자동 완료를 금지한다. +- 이슈별 완료 여부, 완료 시각, 외부 Gitea 이슈 번호·상태를 각각 보존해 감사 추적이 가능해야 한다. + +### 8.5 기본 처리·완료·재문의·재오픈 통합 시퀀스 + +피드백이 생성된 뒤 접수·처리·완료되는 기본 흐름과, 완료 확인 전후에 재문의가 발생하는 예외 흐름을 하나의 시퀀스로 정리한다. 완료 확인 전 재문의는 기존 완료 절차를 취소하고 `진행중`으로 유지하며, 완료 확정 후 재문의는 관리자의 검토와 수동 재오픈을 거친다. + +```mermaid +sequenceDiagram + autonumber + participant F as Q&A 작성자 + participant S as 관리페이지 시스템 + participant A as 관리자 + + F->>S: 작성 페이지에서 Q&A 작성 + S->>S: 작성된 Q&A를 관리페이지의 피드백으로 등록 + S->>S: 피드백 상태를 신규로 설정 + + A->>S: 관리페이지에서 피드백 상세 페이지를 처음 열람 + S->>S: 피드백 상태를 신규에서 접수로 자동 변경 + + A->>S: 피드백 처리 과정에 대한 관리자 댓글 등록 + S->>S: 피드백 상태를 접수에서 진행중으로 자동 변경 + + A->>S: 처리 결과를 알리는 관리자 댓글 등록 + + alt 완료 확인 대기 중 + S->>S: Q&A 작성자의 완료 확인 대기 정보 저장 + Note over S: 피드백 상태는 진행중으로 유지 + S-->>F: 관리페이지에 완료 확인 요청 표시 + S-->>F: NaverWorks(SMS)로 Q&A 작성자에게 완료 확인 요청 알림 발송 + + alt Q&A 작성자가 완료 확인 + F->>S: 처리 결과 확인 버튼 클릭 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + S-->>A: NaverWorks(SMS)로 관리자에게 완료 확정 알림 발송 + else Q&A 작성자가 확인하지 않고 기간 만료 + S->>S: 완료 확인 대기 기간 만료 여부를 자동 확인 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + S-->>F: NaverWorks(SMS)로 Q&A 작성자에게 기간 만료 완료 안내 발송 + S-->>A: NaverWorks(SMS)로 관리자에게 기간 만료 완료 처리 알림 발송 + else 완료 확인 전에 Q&A 작성자가 재문의 + F->>S: 같은 내용에 대한 추가 댓글 또는 재문의 + S->>S: 완료 확인 대기 정보를 해제하고 진행중 상태 유지 + S-->>A: NaverWorks(SMS)로 관리자에게 재문의 알림 발송 + A->>S: 추가 처리 또는 기존 이슈 보완 + end + else 완료 확정 후 동일 문제 재문의 + S->>F: 피드백이 완료 상태임을 표시 + F->>S: 같은 문제에 대한 추가 댓글 작성 + S->>S: 완료 상태를 자동으로 변경하지 않음 + S-->>A: NaverWorks(SMS)로 관리자에게 재문의 검토 알림 발송 + A->>S: 피드백 상태를 완료에서 진행중으로 직접 변경 + A->>S: 재처리 안내 댓글 작성 또는 이슈 재연결 + else 완료 확정 후 새로운 문제 제기 + F->>S: 새로운 내용으로 추가 Q&A 작성 + S->>S: 기존 완료 피드백과 새 Q&A의 연관 정보 기록 + S-->>A: NaverWorks(SMS)로 관리자에게 새 문의 검토 알림 발송 + A->>S: 필요한 경우 기존 피드백을 완료에서 진행중으로 직접 변경 + end +``` + +### 8.6 이슈 처리 및 피드백 연결 시퀀스 + +이슈는 피드백과의 연결 여부, Gitea 개발 이슈 연결 여부에 따라 처리 흐름을 구분한다. + +- 피드백과 연결되지 않은 내부 이슈는 관리자가 관리페이지에서 직접 처리한다. +- 피드백에 연결됐지만 Gitea와 연결되지 않은 내부 이슈는 관리자가 처리하고, 연결된 피드백의 완료 절차까지 이어진다. +- Gitea와 연결된 이슈는 개발자가 Gitea에서 처리하고, 관리자가 결과를 확인한 뒤 연결된 피드백의 완료 절차를 진행한다. +- 하나의 피드백에는 여러 이슈를 연결할 수 있으며, 모든 이슈가 완료되기 전에는 피드백을 완료하지 않는다. + +```mermaid +sequenceDiagram + autonumber + participant F as Q&A
작성자 + participant S as 관리페이지
시스템 + participant A as 관리자 + participant I1 as 내부 이슈
A + participant I2 as 내부 이슈
B + participant G1 as Gitea 이슈
A + participant G2 as Gitea 이슈
B + participant D as 개발자 + + alt 피드백 연결 없이 내부 이슈만 발생 + A->>S: 관리페이지에서 내부 이슈 A 등록 + S->>I1: 내부 이슈 A 생성 요청 + I1-->>S: 내부 이슈 A를 신규 상태로 생성 + A->>S: 내부 이슈 A 상세 페이지 열람 + S->>S: 이슈 A 상태를 신규에서 접수로 자동 변경 + A->>S: 내부 이슈 A 처리 시작 + A->>S: 이슈 A 상태를 접수에서 진행중으로 직접 변경 + A->>S: 내부 이슈 A 처리 완료 + A->>S: 이슈 A 상태를 진행중에서 완료로 직접 변경 + Note over S: 피드백 연결이 없어 Q&A 작성자 완료 확인 절차는 없음 + + else 피드백과 연결된 내부 이슈
(Gitea 연결 없음) + F->>S: 작성 페이지에서 Q&A 작성 + S->>S: 작성된 Q&A를 관리페이지의 피드백으로 등록 + S->>S: 피드백 상태를 신규로 설정 + A->>S: 관리페이지에서 피드백 상세 페이지 열람 + S->>S: 피드백 상태를 신규에서 접수로 자동 변경 + A->>S: 내부 이슈 A를 새로 등록하고 피드백에 연결 + S->>I1: 내부 이슈 A 생성 요청 + I1-->>S: 내부 이슈 A를 신규 상태로 생성 + S->>S: 피드백 상태를 접수에서 진행중으로 자동 변경 + A->>S: 내부 이슈 A 상세 페이지 열람 + S->>S: 이슈 A 상태를 신규에서 접수로 자동 변경 + A->>S: 내부 이슈 A 처리 시작 + A->>S: 이슈 A 상태를 접수에서 진행중으로 직접 변경 + A->>S: 내부 이슈 A 처리 완료 + A->>S: 이슈 A 상태를 진행중에서 완료로 직접 변경 + S->>S: 연결된 이슈가 모두 완료되었는지 확인 + A->>S: 처리 결과를 확인하고 완료 안내 댓글 등록 + S->>S: Q&A 작성자의 완료 확인 대기 정보 저장 + S-->>F: Q&A 작성자에게 완료 확인 요청 표시 + + alt Q&A 작성자가 완료 확인 + F->>S: 처리 결과 확인 버튼 클릭 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + else Q&A 작성자가 확인하지 않고 기간 만료 + S->>S: 완료 확인 대기 기간 만료 여부를 자동 확인 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + end + + else 피드백과 연결된 Gitea 개발 이슈 + F->>S: 작성 페이지에서 Q&A 작성 + S->>S: 작성된 Q&A를 관리페이지의 피드백으로 등록 + S->>S: 피드백 상태를 신규로 설정 + Note over A,S: 관리자가 댓글을 남기면 피드백은 진행중 상태가 됨 + + A->>S: 이슈 A를 새로 등록 + S->>I1: 내부 이슈 A 생성 요청 + I1-->>S: 내부 이슈 A를 신규 상태로 생성 + A->>S: 피드백에 이슈 A 연결 + S->>S: 이슈 A 상태를 신규에서 접수로 자동 변경 + S->>S: 피드백 상태를 신규에서 진행중으로 자동 변경 + A->>S: 이슈 A에 Gitea 이슈 연결 + S->>G1: 내부 이슈와 Gitea 이슈 연결 + S->>S: 외부 이슈 연결 이력 저장 + + D->>G1: 개발자가 Gitea 이슈 A의 내용을 확인 + D->>S: 이슈 A 상태를 진행중으로 직접 변경 + D->>G1: 개발자가 이슈 A를 처리 + D->>S: 이슈 A 상태를 완료로 직접 변경 + + A->>S: 이슈 B를 새로 등록하고
같은 피드백에 연결 + S->>I2: 내부 이슈 B 생성 요청 + I2-->>S: 내부 이슈 B를 신규 상태로 생성 + A->>S: 이슈 B에 Gitea 이슈 연결 + S->>G2: 내부 이슈와 Gitea 이슈 연결 + S->>S: 이슈 B 상태를 신규에서 접수로 자동 변경 + + alt 이슈 A만 완료되고
이슈 B는 아직 처리 중 + S->>S: 다른 이슈가 남아 있으므로 피드백을 진행중으로 유지 + Note over S: 연결된 이슈 일부만 완료되어도 피드백은 완료하지 않음 + D->>G2: 개발자가 Gitea 이슈 B를
확인하고 처리 + D->>S: 이슈 B 상태를 접수에서 진행중으로 직접 변경 + D->>S: 이슈 B 상태를 진행중에서 완료로 직접 변경 + else 연결된 모든 이슈가 완료 + S->>S: 연결된 이슈가 모두 완료되었는지 확인 + A->>S: 처리 결과를 확인하고 완료 안내 댓글 등록 + S->>S: Q&A 작성자의 완료 확인 대기 정보 저장 + S-->>F: Q&A 작성자에게 완료 확인 요청 표시 + + alt Q&A 작성자가 완료 확인 + F->>S: 처리 결과 확인 버튼 클릭 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + else Q&A 작성자가 확인하지 않고 기간 만료 + S->>S: 완료 확인 대기 기간 만료 여부를 자동 확인 + S->>S: 피드백 상태를 진행중에서 완료로 자동 변경 + end + end + end +``` + +### 8.7 보류·외부 상태 충돌 예외 시퀀스 + +```mermaid +sequenceDiagram + autonumber + participant A as 관리자 + participant S as 관리페이지 시스템 + participant G as Gitea + participant D as 개발자 + participant F as Q&A 작성자 + + A->>S: 피드백 또는 이슈 상태를 보류로 직접 설정 + S->>S: 보류 상태 저장 + + par 보류 중 이벤트 + A->>S: 관리자 댓글 작성 + S->>S: 댓글은 저장하고 보류 상태는 유지 + and + D->>G: 개발자가 Gitea 이슈 상태 변경 + G->>S: Gitea 상태 변경 내용을 전달 + S->>S: 보류 상태는 보호하고 동기화 이력만 저장 + and + F->>S: Q&A 작성자가 추가 문의 + S->>S: 문의 내용을 저장 + S-->>A: NaverWorks(SMS)로 관리자에게 보류 중 문의 알림 발송 + end + + A->>S: 관리자가 보류를 해제하고 다음 상태 선택 + alt 처리 재개 + A->>S: 접수 또는 진행중 상태 선택 + S->>S: 선택한 상태로 변경 + else 다시 보류 + A->>S: 보류 상태 유지 + end +``` + +### 8.8 전이 우선순위 및 예외 처리표 + +| 이벤트 | 현재 상태 | 기본 처리 | 예외 | +| ------------------------ | --------------- | ---------------------------------- | ----------------------------------------------------- | +| 피드백 생성 | 없음 | `신규` | 생성 실패 시 상태 전이 없이 재시도·오류 기록 | +| 관리자 최초 열람 | `신규` | `접수` 자동 변경 | `보류`·`완료`는 변경하지 않음 | +| 관리자 댓글 등록 | `신규/접수` | `진행중` 자동 변경 | `보류`는 유지. `완료`는 재오픈 요청만 생성 | +| 이슈 연결 | `신규/접수` | 피드백을 `진행중`으로 자동 변경 | `보류`는 유지하고 관리자 재개 필요 | +| Gitea 이슈 연결 | 이슈 `신규` | 이슈 `접수` 자동 변경 | 이슈 `보류/완료`는 외부 연결만 기록하고 status 보호 | +| 개발자 작업 시작 | 이슈 `접수` | 이슈 `진행중` 수동 변경 | 권한 없는 사용자는 변경 불가 | +| 개발자 작업 완료 | 이슈 `진행중` | 이슈 `완료` 수동 변경 | 다른 연결 이슈가 미완료면 피드백은 `진행중` 유지 | +| 처리 완료 안내 댓글 등록 | 피드백 `진행중` | 완료 확인 대기 메타데이터 기록 | 미완료 이슈·보류 이슈가 있으면 안내 댓글 등록 전 경고 | +| Q&A 작성자 완료 확인 | 완료 확인 대기 | 피드백 `완료` 자동 변경 | 재문의가 먼저 발생하면 완료 처리 취소 | +| 확인 기간 만료 | 완료 확인 대기 | 피드백 `완료` 자동 변경 | 보류로 수동 전환된 경우 자동 완료 금지 | +| 완료 후 동일 문제 재문의 | 피드백 `완료` | 재오픈 검토 알림 | 피드백은 관리자가 수동으로 `진행중` 전환 | +| 보류 설정 | 모든 상태 | 관리자 수동으로 `보류` | 자동 이벤트가 덮어쓰지 않음 | +| 보류 해제 | `보류` | 관리자 수동으로 `접수/진행중` 선택 | 자동으로 이전 상태를 추정하지 않음 | + +### 8.9 자동화 구현 시 필수 데이터 + +- 피드백 상태, 상태 변경자, 상태 변경 시각 +- 이슈별 상태, 상태 변경자, 상태 변경 시각 +- 피드백-이슈 연결·해제 이력 +- Gitea 이슈 번호, URL, 마지막 외부 상태 동기화 시각 +- `completion_requested`, `completion_requested_at`, `completion_requested_by` +- `completion_notice_comment_id` 및 처리 완료 안내 댓글의 원문 +- Q&A 작성자의 완료 확인 시각과 확인 사용자 +- 완료 확인 만료 기준 시각 및 자동 완료 처리 시각 +- 완료 이후 재문의 여부와 관리자 재오픈 여부 +- 보류 설정자, 보류 사유, 보류 시작·해제 시각 +- 자동화 이벤트의 원본 이벤트 ID와 처리 결과 + +### 8.10 구현 전 합의가 필요한 정책 + +1. 완료 확인 대기 기간을 며칠로 할지, 프로젝트별로 다르게 둘지 결정한다. +2. 완료 확인 대기 중 작성자 재문의가 발생하면 즉시 `진행중`으로 되돌릴지 결정한다. 본 문서는 즉시 되돌리는 안을 기준으로 한다. +3. 완료 상태에서 작성자의 추가 댓글을 새 피드백으로 분리할지, 기존 피드백의 재오픈 요청으로 처리할지 결정한다. +4. 여러 이슈 중 하나가 보류일 때 피드백 전체를 보류로 자동 표시할지 결정한다. 본 문서는 피드백 status 자동 변경은 하지 않고 완료만 차단하는 안을 기준으로 한다. +5. 이슈 연결을 피드백 `신규/접수`에서도 허용할지, `진행중`에서만 허용할지 결정한다. 본 문서는 연결 시 `진행중`으로 자동 승격하는 안을 기준으로 한다. +6. Gitea 웹훅으로 이슈 상태를 자동 반영할 범위와 개발자의 수동 변경을 병행할지 결정한다. +7. 처리 완료 안내 댓글을 일반 댓글과 구분하기 위한 `completion_notice` 선택 UI를 둘지, 특정 댓글 템플릿으로 제한할지 결정한다. 본 문서는 댓글 작성 영역의 유형 선택 UI를 기준으로 한다. + +## 9. 피드백·이슈 자동화 구현 작업계획 + +### 9.1 구현 목표 + +- 8장의 시퀀스와 전이 우선순위를 실제 관리페이지 처리 흐름에 반영한다. +- 피드백과 이슈의 상태 변경을 화면별 임의 처리에서 공통 상태 전이 규칙으로 통합한다. +- 자동 변경과 관리자 수동 변경을 구분하고, 모든 전이 결과와 실패 원인을 추적한다. +- 기존 피드백 원문·댓글·이슈 연결 데이터는 보존하고, 단계별로 호환성을 확인한다. +- 이번 구현에서는 PDF를 수정하지 않고 MD를 기준 문서로 사용한다. + +### 9.2 현재 구조와 상태값 호환 계획 + +현재 코드는 피드백·이슈에 각각 6개 상태값을 사용한다. 8장의 업무 상태는 5단계이므로 데이터 마이그레이션과 API/UI 호환 처리가 필요하다. + +| 현재 코드 상태 | 새 업무 상태 | 처리 원칙 | +| --- | --- | --- | +| `INIT` | `NEW` | 신규 작성·생성 직후 상태 | +| `ON_REVIEW` | `RECEIVED` | 관리자가 확인하기 전후의 기존 검토 상태를 접수로 통합 | +| `DETAILED_REVIEW` | `RECEIVED` | 상세 검토 상태를 접수로 통합하고 별도 상태로 유지하지 않음 | +| `IN_PROGRESS` | `IN_PROGRESS` | 처리 진행 상태 유지 | +| `RESOLVED` | `COMPLETED` | 완료 상태로 통합 | +| `PENDING` | `ON_HOLD` | 관리자 보류 상태로 통합 | + +- DB 기존 값은 마이그레이션으로 새 코드로 치환한다. +- API 응답·검색·필터·통계·화면 문구는 새 5단계만 노출한다. +- `ON_HOLD`의 설정·해제는 관리자 수동 전이만 허용한다. +- `COMPLETED`는 단순 열람·댓글·외부 동기화로 자동 재오픈하지 않는다. +- 전이 로직은 피드백과 이슈에서 동일한 우선순위와 보호 규칙을 사용한다. + +### 9.3 단계별 작업 목록 + +현재 진행 상태: 0~6단계 핵심 흐름과 외부 Q&A 작성 API 연계를 구현함. 상태 5단계 통합, 최초 열람, 관리자 댓글·이슈 연결에 따른 자동 전환, 완료 안내 댓글, 작성자 완료 확인, 7일 만료 자동 완료, 다중 이슈 완료 조건, 보류 메타데이터, 재문의 처리, 관리자 알림, 실패 표시, 멱등 처리를 반영함. 스테이징 권한·외부 연동·기존 데이터 건수 검증은 배포 환경에서 진행함. + +#### 0단계. 기준선 고정 및 전이 계약 작성 + +- [x] 현재 피드백/이슈 상태의 저장 위치, 변경 API, 화면별 직접 변경 지점을 목록화 +- [x] `NEW → RECEIVED → IN_PROGRESS → COMPLETED` 기본 전이와 `ON_HOLD` 보호 규칙을 공통 전이표로 코드화 +- [x] 자동 전이와 관리자 수동 전이를 구분하는 입력값 정의 + - `actorType`: `SYSTEM`, `ADMIN`, `USER`, `DEVELOPER` + - `trigger`: 생성, 최초 열람, 댓글, 이슈 연결, 완료 안내, 완료 확인, 기간 만료, 재문의, 외부 동기화 등 +- [x] 같은 이벤트가 반복되어도 결과가 중복 생성되지 않도록 멱등성 기준 정의 +- [x] 권한 없는 사용자의 전이·보류 해제·완료 후 재오픈 차단 + +#### 1단계. 데이터 모델 및 마이그레이션 + +- [x] 피드백 상태를 새 5단계로 정리하고 기존 6단계 데이터를 매핑 +- [x] 이슈 상태를 새 5단계로 정리하고 기존 6단계 데이터를 매핑 +- [x] 피드백 자동화 메타데이터 저장 구조 추가 + - 완료 확인 요청 여부·요청 시각·요청 댓글 ID + - 완료 확인 시각·확인 사용자 + - 완료 확인 만료 기준 시각·자동 완료 시각 + - 재문의 및 수동 재오픈 여부 +- [x] 보류 정보 저장 구조 추가 + - 보류 설정자·사유·시작 시각·해제 시각·해제 후 선택 상태 +- [x] 피드백-이슈 연결·해제 이력과 이슈별 완료 시각 보존 +- [x] 기존 데이터가 손실되지 않는 `up/down` 마이그레이션 작성 +- [x] 완료 확인에 따른 상태·완료 메타데이터 변경을 하나의 트랜잭션 경계에서 저장 + +#### 2단계. 공통 상태 전이 서비스 + +- [x] 피드백 상태 전이 서비스 구현 +- [x] 이슈 상태 전이 서비스 구현 +- [x] 현재 상태, 요청 주체, 이벤트, 연결 이슈 상태를 함께 검사하는 guard 구현 +- [x] `ON_HOLD` 자동 변경 차단 +- [x] `COMPLETED` 자동 재오픈 차단 및 관리자 수동 재오픈 지원 +- [x] 다중 이슈 집계 구현 + - 미완료 이슈가 하나라도 있으면 피드백 완료 차단 + - 보류 이슈가 하나라도 있으면 피드백 완료 차단 + - 연결 해제만으로 피드백을 완료하지 않음 +- [x] 상태 변경 이력과 자동화 이벤트 처리 결과 저장 +- [x] 기존 직접 상태 수정 API가 공통 전이 서비스를 거치도록 변경 + +#### 3단계. 자동 이벤트 연결 + +- [x] 피드백 생성 시 `NEW` 자동 설정 +- [x] 관리자가 피드백 상세를 최초 열람하면 `NEW → RECEIVED` 자동 변경 +- [x] 관리자가 공개 댓글을 등록하면 `NEW/RECEIVED → IN_PROGRESS` 자동 변경 +- [x] 이슈 연결 시 피드백을 `IN_PROGRESS`로 자동 변경 +- [x] 이슈 생성 시 `NEW` 설정 +- [x] 관리자가 이슈를 최초 열람하거나 Gitea 이슈를 연결하면 `NEW → RECEIVED` 자동 변경 +- [x] 개발자 작업 시작·완료는 개발자 권한의 수동 전이로 처리 +- [x] 처리 완료 안내 댓글 등록 시 완료 확인 요청 메타데이터 생성 +- [x] 완료 확인 대기 중 재문의 발생 시 대기 정보 해제 및 `IN_PROGRESS` 유지 +- [x] 완료 확정 후 재문의 발생 시 알림만 자동 처리하고 관리자의 수동 재오픈을 요구 +- [x] Gitea 상태 동기화가 `ON_HOLD` 상태를 덮어쓰지 않도록 보호 + +#### 4단계. 완료 확인 및 만료 처리 + +- [x] Q&A 작성자 상세 화면에 처리 결과 확인 동작 추가 +- [x] 완료 확인 전용 API를 멱등하게 구현 +- [x] 완료 확인 시 `IN_PROGRESS → COMPLETED` 자동 변경 +- [x] 완료 확인 대기 기간은 프로젝트 설정값으로 분리하고 초기 기본값은 7일로 적용 +- [x] 만료 대상 조회 배치 구현 +- [x] 다중 인스턴스 실행을 고려한 스케줄러 락 적용 +- [x] 만료 처리 성공·실패·재시도 이력 저장 +- [x] 완료 확인 및 만료 시 Q&A 작성자·관리자 알림 처리 + +#### 5단계. 관리자 처리 화면 반영 + +- [x] 피드백 상세 팝업에서 상태·담당자·이슈 연결·댓글 유형을 전이 규칙에 맞게 제공 +- [x] 처리 완료 버튼은 추가하지 않고, `처리 완료 안내 댓글`로 완료 확인 요청 생성 +- [x] 완료 확인 대기 중인 피드백에 대기 배지·요청 시각 표시 +- [x] 이슈 상세 팝업에서 상태 변경 시 수동/자동 전이 규칙 적용 +- [x] 보류 설정 시 사유 입력, 보류 해제 시 `접수/진행중` 선택 제공 +- [x] 완료 상태의 재오픈은 관리자 수동 동작으로만 제공 +- [x] 다중 이슈 연결 시 개별 이슈 상태와 피드백 완료 가능 여부 표시 +- [x] 자동 전이 실패 시 관리자에게 원인과 수동 처리 필요 여부 표시 + +#### 6단계. 목록·통계·알림 반영 + +- [x] 목록의 상태 필터·정렬·요약 박스를 새 5단계 기준으로 변경 +- [x] 기본 목록에서는 완료·보류를 제외하되 검색으로 조회 가능하도록 유지 +- [x] 피드백/이슈 상태, 연결 이슈 수, 완료 확인 대기 여부를 목록 응답에 포함 +- [x] 상태 변경·댓글·이슈 연결·완료 확인·재문의 알림을 기존 관리페이지 시스템 알림 흐름에 연결 +- [x] NaverWorks(SMS)는 별도 시퀀스 참여자가 아닌 관리페이지 시스템의 알림 처리 결과로 기록 +- [x] 로컬 마이그레이션 후 대시보드용 상태 매핑·집계 조회가 기존 데이터와 호환되는지 검증 + +#### 7단계. 검증 및 전환 + +- [x] 상태 전이 단위 테스트 작성 +- [x] 다중 이슈·보류·완료 확인 대기·재문의·완료 후 재오픈 워크플로 테스트 작성 +- [x] 댓글 중복 등록과 완료 확인 중복 요청의 멱등성 테스트 작성. 이슈 연결은 DB 중복 검사까지 반영 +- [x] 로컬 Docker DB에서 마이그레이션 전후 상태·스키마를 비교 +- [ ] 관리자/개발자/Q&A 작성자 권한별 화면·API 접근 검증 +- [ ] Gitea 웹훅 지연·중복·실패 상황 검증 +- [x] 단계적 적용을 위한 롤백 기준 정의 +- [x] 운영 전환 체크리스트와 장애 시 수동 처리 절차 작성 + +### 9.4 우선 구현 순서 + +1. 상태값 5단계 호환 및 공통 전이 guard +2. 피드백 댓글·이슈 연결·이슈 상태 변경 이벤트 연결 +3. 다중 이슈 집계와 보류 보호 +4. 완료 안내 댓글·Q&A 작성자 완료 확인·만료 처리 +5. 목록·상세 화면과 알림 반영 +6. 마이그레이션·통합 테스트·운영 전환 + +### 9.5 이번 작업의 완료 기준 + +- [x] 시퀀스에 정의된 기본 흐름이 실제 API 이벤트와 상태 변경으로 재현됨 +- [x] 피드백과 이슈 모두 `신규/접수/진행중/완료/보류`만 사용함 +- [x] 보류 상태는 자동 이벤트가 변경하지 않음 +- [x] 한 피드백에 여러 이슈가 연결되어도 모든 이슈 완료 전 피드백이 완료되지 않음 +- [x] 관리자의 처리 완료 안내 댓글 이후 Q&A 작성자 확인 또는 기간 만료를 거쳐서만 피드백이 완료됨 +- [x] 완료 확인 전 재문의와 완료 후 재문의가 서로 다른 규칙으로 처리됨 +- [x] 상태 변경·자동 처리·알림·실패 이력이 조회 가능함 +- [x] 기존 데이터와 권한 모델을 유지하면서 애플리케이션 롤백이 가능하도록 배포 절차와 DB 보존 원칙을 문서화함 diff --git a/docs/관리페이지 md 파일/api-packaging-tasks.md b/docs/관리페이지 md 파일/api-packaging-tasks.md new file mode 100644 index 0000000..3bca7b8 --- /dev/null +++ b/docs/관리페이지 md 파일/api-packaging-tasks.md @@ -0,0 +1,350 @@ +# API 패키징 작업 계획 + +- 작성일: 2026-08-25 +- 목적: 현재 ABC User Feedback와 커스터마이징 기능을 다른 서비스에서 사용할 수 있는 공식 API 패키지로 정리한다. +- 현재 상태: 기능별 API는 대부분 존재하고 Swagger도 연결되어 있으나, 외부 배포용 API 계약과 스키마는 아직 정리되지 않았다. + +## 1. 현재 구조 + +### 1.1 NestJS API + +| 구분 | 현재 주소 | 인증 | Swagger | +|---|---|---|---| +| 공개 연동 API | /api/... | x-api-key | /docs | +| 관리자 API | /api/admin/... | JWT + 권한 | /admin-docs | +| Swagger JSON | /docs-json, /admin-docs-json | 서버 설정에 따름 | 생성 가능 | + +현재 로컬 확인 주소: + +- 공개 API 문서: http://127.0.0.1:4000/docs +- 관리자 API 문서: http://127.0.0.1:4000/admin-docs +- 공개 OpenAPI JSON: http://127.0.0.1:4000/docs-json +- 관리자 OpenAPI JSON: http://127.0.0.1:4000/admin-docs-json + +Swagger 설정은 apps/api/src/main.ts에 있으며, 공개 문서 생성 스크립트는 apps/api/src/scripts/build-swagger-docs.ts에 있다. + +### 1.2 Secretary API + +Secretary API는 FastAPI 자동 문서를 사용한다. + +- Swagger UI: :8010/docs +- OpenAPI JSON: :8010/openapi.json + +현재 Secretary API에는 SSO 기반 접근 정보, workspace, ticket, 댓글, 내부 메모, 첨부파일, 담당자 관련 API가 포함되어 있다. + +### 1.3 Next.js API Route + +apps/web/src/pages/api/support/* 아래의 API는 화면과 Secretary API 사이를 연결하는 내부 프록시다. + +이 경로들은 NestJS Swagger 문서에는 포함되지 않는다. 외부 패키지에서 직접 사용할 공식 API로 제공하려면 NestJS 또는 Secretary API의 공개 계약으로 승격해야 한다. + +## 2. 현재 구현된 API 범위 + +### 2.1 공개 피드백 API + +- 프로젝트·채널 조회 +- 채널 필드 조회 +- 피드백 생성 +- 피드백 목록 조회 +- 피드백 검색 +- 피드백 상세 조회 +- 피드백 수정·삭제 +- 피드백과 이슈 연결·해제 +- 댓글 CRUD +- 내부 댓글 구분 +- 댓글 첨부파일 업로드·조회·삭제 +- 피드백 첨부 이미지 업로드 +- 카테고리 조회·관리 +- 이슈 생성·조회·검색·수정·삭제 + +관련 컨트롤러: + +- apps/api/src/domains/api/feedback.controller.ts +- apps/api/src/domains/api/v2/feedback.controller.ts +- apps/api/src/domains/api/issue.controller.ts +- apps/api/src/domains/api/channel.controller.ts +- apps/api/src/domains/api/project.controller.ts + +### 2.2 관리자 API + +- 프로젝트·채널·필드 관리 +- 피드백 관리자 검색·수정·삭제 +- 피드백 상태 처리 +- 피드백 담당자 지정 +- 이슈 관리자 지정 +- 이슈 상태 처리 +- Gitea 이슈 생성·연결·동기화 +- 댓글·내부 메모 관리 +- 통계 조회 +- 다중 프로젝트 통합 대시보드 +- 관리자 권한·역할 관리 + +통합 대시보드 API: + + GET /api/admin/dashboard/overview + +현재 대시보드 API는 사용자의 프로젝트 권한을 기준으로 프로젝트별 피드백·이슈 요약과 Todo 항목을 집계한다. + +### 2.3 Secretary API + +- SSO 사용자 접근 정보 +- workspace 목록 및 폼 템플릿 +- ticket 생성·조회·상세 조회 +- 댓글 CRUD +- 내부 메모 CRUD +- 담당자 후보 조회 및 지정 +- 이슈 연결 +- 승인 처리 +- 첨부파일 저장·조회 +- R2 기반 파일 저장 + +## 3. 현재 Swagger 문서화 상태 + +### 완료된 부분 + +- [x] NestJS Swagger 모듈 연결 +- [x] 공개 API 문서와 관리자 API 문서 분리 +- [x] 공개 API Key 인증 정의 +- [x] 관리자 JWT 인증 정의 +- [x] 주요 피드백·이슈 API의 ApiTags, ApiParam, ApiOperation 일부 적용 +- [x] 일부 요청·응답 DTO에 ApiProperty 적용 +- [x] FastAPI 자동 OpenAPI 문서 제공 +- [x] 공개 Swagger JSON 생성 스크립트 존재 + +### 보완이 필요한 부분 + +- [x] 통합 대시보드 응답 DTO 정의 +- [x] 통합 대시보드 날짜·프로젝트 필터 파라미터 문서화 +- [x] 관리자 피드백 컨트롤러에 ApiTags와 상세 operation 문서 추가 +- [x] 댓글 CRUD 요청·응답 DTO 정의 +- [x] 내부 메모와 일반 댓글의 공개 범위 문서화 +- [x] 첨부파일 multipart 요청 스키마 정의 +- [x] 담당자·상태·우선순위 enum 문서화 +- [x] 오류 응답 형식 표준화 +- [x] 동적 필드의 요청·응답 계약 정의 +- [x] 내부 workspace mapping API의 Swagger 공개 범위 재검토 +- [x] Next.js 내부 프록시 API와 공식 API의 역할 분리 + +### 1차 보완 완료 기록 + +- `DashboardOverviewResponseDto`와 하위 통계·Todo·상태 스키마를 추가했다. +- `GET /api/admin/dashboard/overview`의 날짜 범위와 쉼표 구분 프로젝트 필터를 Swagger에 명시했다. +- 관리자 피드백 API에 `admin-feedbacks` 태그, 경로 파라미터, operation 설명, 동적 body 예시, 생성·수정·삭제 응답 설명을 추가했다. +- 공개 OpenAPI JSON과 관리자 OpenAPI JSON을 함께 생성하도록 `apps/api/src/scripts/build-swagger-docs.ts`를 보완했다. +- 공통 오류 응답, enum 문서화, 동적 필드 계약은 다음 보완 작업으로 남겨두었다. + +### 2차 보완 완료 기록 + +- Secretary 댓글 요청·응답 모델에 작성자, 공개 범위, 첨부파일 메타데이터, 수정·삭제 가능 여부의 설명을 추가했다. +- `GET/POST/PUT/DELETE /api/tickets/{ticketId}/comments...`를 공개 댓글 계약으로 명시하고, `GET/POST/PUT/DELETE /api/tickets/{ticketId}/internal-memos...`를 관리자 전용 계약으로 분리했다. +- 공개 댓글 API는 관리자 권한으로 호출하더라도 `is_internal=false`로 저장되며, 내부 메모는 전용 API에서만 생성되도록 경계를 고정했다. +- 지원 요청 생성 API에 JSON과 multipart/form-data 계약을 모두 문서화하고, 반복 가능한 `attachments` binary 필드와 JSON 문자열 `extra_fields`를 명시했다. +- 첨부파일 댓글 API도 `content`와 반복 가능한 `attachments` 입력을 OpenAPI에 노출하도록 명시적인 multipart 파라미터로 변경했다. +- 삭제 응답을 `TicketDeleteResponse` DTO로 고정해 `deleted`, `ticket_id`, `comment_id`, `feedback_id`를 문서화했다. +- Secretary API는 FastAPI 자동 OpenAPI 엔드포인트(`/openapi.json`)에서 위 계약을 산출하며, 실행 환경에 의존성 설치 후 `/docs`에서 확인한다. + +### 3차 보완 완료 기록 + +- 피드백 상태 enum을 `INIT`, `ON_REVIEW`, `DETAILED_REVIEW`, `IN_PROGRESS`, `RESOLVED`, `PENDING`으로 고정하고, 이슈 상태와 동일한 6단계 값임을 Swagger에 명시했다. +- 피드백 우선순위 enum을 `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`로 문서화했다. +- 관리자 피드백 수정 API에 상태, 우선순위, 담당자 ID·tenant ID·이름·이메일 필드의 요청 예시와 enum을 추가했다. +- 통합 대시보드 Todo 응답에 담당자 정보를 포함하고, 피드백 상태·우선순위 타입을 enum으로 제한했다. +- NestJS와 Secretary API의 오류 응답을 `code`, `message`, `error`, `statusCode`, `path` 공통 구조로 표준화했다. 추가 오류 정보는 `details`에 담는다. +- NestJS 관리자 피드백·통합 대시보드 API와 Secretary API OpenAPI에 공통 오류 응답 스키마를 노출했다. + +### 4차 보완 완료 기록 + +- 채널 필드 조회 API(`/projects/{projectId}/channels/{channelId}/fields`)를 동적 피드백 계약의 기준점으로 명시했다. 클라이언트는 이 응답으로 필드 키, format, property, status, select 옵션을 먼저 확인한다. +- 공개·관리자 피드백 생성 API에 동적 JSON 요청 스키마를 연결했다. `title`, `contents`, `Category`, `IP`, `MAC_address`는 대표 예시이며 실제 입력 키는 채널 설정을 따른다. +- `issueNames`는 피드백 필드로 저장되지 않는 이슈 연결용 제어 필드임을 문서화했다. +- 이미지 첨부 생성 API에 multipart 스키마를 연결하고, 동적 비파일 필드와 반복 가능한 `images` binary 파일 입력을 구분했다. +- 피드백 검색 응답은 실제 구현처럼 동적 필드가 최상위에 펼쳐지는 `items`와 페이지네이션 `meta` 구조로 문서화했다. +- 동적 검색 `query` 값에 문자열·숫자·배열·`gte/lt` 범위 조건을 문서화했다. +- 관리자 피드백 수정 API는 공통 관리 필드(상태·우선순위·담당자)를 명시하면서, 나머지 허용 필드는 채널 필드 설정에서 발견하도록 설명했다. + +### 5차 보완 완료 기록 + +- Secretary API가 ABC API의 `GET /api/internal/support/workspace-mappings`를 서비스 간 동기화에만 사용하도록 확인했다. +- workspace mapping endpoint는 `MASTER_API_KEY` 서비스 인증을 사용하므로 공개·관리자 Swagger에서 제외했다. 이 키는 외부 패키지 소비자에게 제공하는 API Key가 아니다. +- Next.js의 `/api/support/*` 경로는 브라우저 세션과 Secretary API 사이의 내부 BFF(proxy)로 분류하고, 외부 패키지용 공식 API는 Secretary API와 NestJS 공개·관리자 API로 한정했다. +- Next.js proxy 경로는 Swagger 산출 대상이 아니며, 외부 연동 문서와 SDK 계약에 포함하지 않는다는 경계를 명시했다. + +## 4. 외부 패키지용 API 계약 설계 + +### 4.1 API 영역 분리 + +외부 패키지에서는 다음 세 영역을 분리한다. + +1. 사용자용 피드백 API +2. 관리자용 운영 API +3. 내부 Secretary 업무 API + +외부 사용자에게 관리자 API나 내부 API 권한이 노출되지 않도록 인증 체계와 문서도 분리한다. + +### 4.2 권장 버전 정책 + + /api/v1/projects/{projectId}/channels/{channelId}/feedbacks + /api/v1/projects/{projectId}/issues + /api/v1/admin/... + +기존 경로는 호환성을 위해 유지하고, 패키지용 공식 계약은 v1 버전으로 고정하는 것을 권장한다. + +### 4.3 공통 응답 형식 + +성공 응답과 오류 응답을 모든 API에서 일관되게 정의한다. + + { + "data": {}, + "meta": { + "requestId": "..." + } + } + +오류 예시: + + { + "code": "NOT_FOUND", + "message": "Feedback was not found.", + "error": "NOT_FOUND", + "statusCode": 404, + "path": "/api/feedbacks/34", + "details": {} + } + +### 4.4 페이지네이션·검색·정렬 + +- page, limit 또는 cursor 방식 중 하나로 표준화 +- 최대 limit 제한 +- 정렬 가능 필드 화이트리스트 적용 +- 제목·내용·작성자 검색의 검색 범위 명시 +- 비밀글 접근 시 작성자·관리자 권한 검증 +- 응답에 total, page, limit, hasNext 포함 + +### 4.5 동적 필드 + +현재 피드백 데이터는 채널 필드 설정에 따라 동적으로 구성된다. + +현재 API 계약: + + { + "title": "...", + "contents": "...", + "Category": "ERROR_QNA", + "IP": "...", + "MAC_address": "...", + "issueNames": ["Login error"] + } + +- 실제 필드 키와 허용 값은 `GET /projects/{projectId}/channels/{channelId}/fields`에서 확인한다. +- `text`, `keyword`, `aiField`는 문자열, `number`는 숫자, `select`는 옵션 key 또는 null, `multiSelect`와 `images`는 배열, `date`는 날짜 문자열 또는 null을 사용한다. +- 응답에서도 채널 필드 값은 최상위 동적 속성으로 반환되며, `id`, `createdAt`, `updatedAt`, `issues`가 함께 제공된다. +- `issueNames`는 생성 요청에서만 사용하는 이슈 연결 제어 필드다. +- 장기적으로 SDK 호환성을 위해 `customFields` 래퍼를 도입할 수 있지만, 기존 클라이언트 호환성을 깨지 않도록 별도 버전 계약으로 진행한다. + +## 5. 인증·권한·보안 + +- [ ] 공개 API Key를 프로젝트·채널 단위로 제한 +- [ ] 관리자 JWT와 공개 API Key의 권한 차이 문서화 +- [ ] Secretary API는 SSO JWT 검증을 필수화 +- [ ] workspace·tenant·project 접근 범위 검증 +- [ ] 비밀글 조회 권한을 API 레벨에서 강제 +- [x] 내부 댓글·내부 메모가 일반 사용자 응답에 포함되지 않도록 보장 +- [ ] R2 presigned URL 만료 시간 문서화 +- [ ] API Key·JWT·R2 credential 로그 마스킹 확인 +- [ ] rate limit과 업로드 용량 제한 추가 +- [ ] CORS와 외부 패키지 허용 origin 정책 정의 + +### 보안 검증 기록 + +- 공개 API의 일반 댓글 목록·생성·첨부파일 접근은 내부 메모를 포함하지 않도록 고정했다. `is_internal` 입력과 `includeInternal` 조회 플래그는 공개 API에서 무시하며, 내부 메모는 관리자 전용 API에서만 처리한다. +- 공개 API Key는 `projectId` 기준으로 검증하고, `MASTER_API_KEY`는 서비스 간 내부 호출을 위한 전역 키로 구분했다. +- 관리자 API는 JWT guard와 권한 guard를 사용하고, Secretary API는 SSO JWT의 서명·만료·`sso_sub`·`tenant_id`를 검증한다. + +## 6. Swagger/OpenAPI 산출물 및 배포 + +- [x] 공개 API OpenAPI JSON 생성 +- [x] 관리자 API OpenAPI JSON 생성 +- [x] Secretary API OpenAPI JSON export (`/openapi.json` 자동 산출) +- [ ] OpenAPI 파일을 패키지에 포함할지 결정 +- [ ] Swagger UI를 staging에서만 노출할지 결정 +- [ ] production에서는 인증된 관리자만 Swagger 접근 가능하도록 제한 +- [ ] API 버전별 문서 URL 제공 +- [ ] Postman collection 또는 SDK 생성 여부 결정 +- [ ] 외부 패키지 배포 시 changelog와 breaking change 정책 추가 + +생성 산출물 후보: + + packages/api-contract/openapi/public.json + packages/api-contract/openapi/admin.json + packages/api-contract/openapi/secretary.json + +## 7. 테스트 계획 + +### 계약 테스트 + +- [ ] OpenAPI schema validation +- [ ] 요청 DTO validation +- [ ] 응답 DTO serialization +- [x] 오류 응답 형식 검증 +- [ ] 페이지네이션·정렬·검색 검증 +- [ ] 비밀글 권한 검증 +- [ ] 내부 댓글 노출 차단 검증 + +### 기능 테스트 + +- [ ] 사용자 피드백 생성 +- [ ] 피드백 목록·상세·수정·삭제 +- [ ] 댓글 CRUD +- [ ] 내부 메모 CRUD +- [ ] 첨부파일 R2 업로드·다운로드·삭제 +- [ ] 피드백 담당자 지정 +- [ ] 상태 변경 +- [ ] 이슈 연결·해제 +- [ ] Gitea 이슈 생성·동기화 +- [ ] 통합 대시보드 조회 +- [ ] 다중 프로젝트 권한 필터링 +- [ ] SSO 재로그인 후 사용자 정보 유지 + +### 배포 전 확인 + +- [ ] pnpm typecheck +- [ ] pnpm lint +- [ ] API 단위 테스트 +- [ ] Secretary API 테스트 +- [ ] E2E 테스트 +- [ ] staging에서 OpenAPI JSON 확인 +- [ ] 실제 외부 클라이언트 샘플 호출 +- [ ] 마이그레이션 및 기존 데이터 호환성 확인 + +## 8. 권장 작업 순서 + +1. 공개 API와 관리자 API의 패키징 범위 확정 +2. API 버전과 인증 정책 확정 +3. 통합 대시보드 응답 DTO 작성 +4. 피드백·댓글·내부메모·첨부파일 DTO 작성 +5. 상태·우선순위·담당자 enum 정리 +6. Swagger decorator 보완 +7. Next.js 내부 프록시와 공식 API 경계 정리 +8. OpenAPI JSON을 별도 계약 패키지로 export +9. 계약 테스트와 권한 테스트 추가 +10. staging 배포 및 외부 호출 검증 +11. SDK 또는 Postman collection 생성 +12. API 패키지 버전 태그 및 배포 + +## 9. 완료 기준 + +- 외부 시스템이 Swagger 문서만 보고 피드백을 생성·조회·수정할 수 있다. +- 관리자 시스템이 피드백 상태·담당자·댓글·내부 메모·이슈·Gitea를 API로 처리할 수 있다. +- 사용자·관리자·내부 API의 권한이 분리되어 있다. +- 동적 필드와 비밀글 정책이 문서화되어 있다. +- 모든 주요 API의 요청·응답·오류 스키마가 OpenAPI에 표시된다. +- Swagger JSON이 CI에서 생성되고 버전 관리 또는 패키지 산출물로 배포된다. +- 기존 화면과 API 소비처의 호환성이 깨지지 않는다. + +## 참고 문서 + +- SSOT 피드백 재구성 작업: docs/ssot-feedback-rearchitecture-tasks.md +- 다중 프로젝트 관리자 통합 대시보드 설계: docs/multi-project-admin-dashboard-design.md +- Secretary SSO 구성 설계: docs/architecture_secretary_sso_components_v2.md +- Q&A 플랫폼 작업 목록: docs/qna-platform-prototype-2-feedback-tasks.md +- NestJS API README: apps/api/README.md diff --git a/docs/관리페이지 md 파일/architecture.md b/docs/관리페이지 md 파일/architecture.md new file mode 100644 index 0000000..eb06a21 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture.md @@ -0,0 +1,308 @@ +# 통합 지원 플랫폼 상세 설계서 + +## 1. 프로젝트 개요 + +### 1.1 프로젝트 정보 + +| 항목 | 내용 | +| --- | --- | +| 플랫폼명 | BARON Q&A System | +| 핵심 목적 | EG-BIM 등 복수 소프트웨어의 Q&A, FAQ, 원격 지원 기능을 통합하고 BARON-SSO 기반 보안을 적용한 전사 기술지원 허브 구축 | +| 주요 대상 | BARON-SSO에 등록된 일반 사용자(User), 소프트웨어 담당자(Support), 시스템 관리자(Admin) | + +### 1.2 추진 배경 및 기대 효과 + +- 여러 제품군에 분산된 기술지원 채널의 단일 플랫폼 통합 +- 사용자 질문, FAQ, 원격 지원 이력의 일원화를 통한 대응 품질 및 추적성 향상 +- BARON-SSO와 앱 단위 권한 제어를 통한 보안성 및 운영 효율 확보 + +## 2. 기술 아키텍처 + +### 2.1 기술 스택 + +| 구분 | 기술 | +| --- | --- | +| Frontend | Next.js, React, Tailwind CSS, Headless UI | +| Backend | FastAPI, Python, SQLAlchemy, Pydantic | +| Database | PostgreSQL | +| 인증/권한 | BARON-SSO, OAuth 2.0, OpenID Connect | +| 외부 연동 | ABC User Feedback 웹훅, 네이버웍스 알림 연동 | +| 인프라 | Ubuntu 24.04 (WSL2), Docker Compose, Nginx | + +### 2.2 아키텍처 방향 + +- 프론트엔드의 Next.js 기반 구성으로 사용자 경험 및 생산성 확보 +- 백엔드의 FastAPI 중심 경량 API 구조 설계를 통한 인증, 게시판, FAQ, 원격 지원 기능 분리 구현 +- PostgreSQL과 SQLAlchemy 기반 데이터 계층 구성 및 앱별 접근 제어의 데이터 모델 반영 +- BARON-SSO 연동 전제의 인증 구조 적용 및 표준 OAuth 2.0 / OIDC 기반 세션·토큰 검증 수행 +- ABC User Feedback 연계를 통한 사용자 의견 수집 및 지원 품질 개선 체계 확보 +- ABC User Feedback 웹훅 이벤트와 네이버웍스 알림 연계를 통한 실시간 커뮤니케이션 체계 확보 + +### 2.3 CI/CD 및 배포 프로세스 + +```mermaid +graph TD + A[개발자: 코드 작성 및 로컬 테스트] --> B[Main 브랜치 Push] + B --> C{GitHub Actions} + C --> D[Lint 및 Unit Test 실행] + D --> E[Docker Image 빌드] + E --> F[Container Registry 저장] + F --> G[운영 서버 배포] + G --> H[Nginx Proxy 라우팅] + H --> I[서비스 가동 및 모니터링] +``` + +## 3. 데이터베이스 설계 + +### 3.1 설계 원칙 + +권한 기반 데이터 격리(RBAC)를 위해 모든 게시물이 `app_id`를 참조하고, 사용자는 `user_app_access`를 통해 허용된 앱에만 접근하도록 설계 + +- 신규 앱이 추가되더라도 공통 스키마 변경 없이 `software_apps` 데이터 추가만으로 확장 가능한 구조 적용 +- 전역 관리자 권한과 앱별 운영 권한 분리를 통한 멀티앱 확장 대응 +- 사용자-앱 매핑의 중복 방지 및 상태값 표준화를 통한 운영 일관성 확보 +- 목록 조회 성능 확보를 위한 앱 기준 인덱스 설계 반영 + +### 3.2 핵심 스키마 + +```sql +-- 1. 소프트웨어 정보 +CREATE TABLE software_apps ( + id SERIAL PRIMARY KEY, + app_code VARCHAR(50) UNIQUE NOT NULL, + app_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 사용자 정보 (BARON-SSO 동기화) +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + sso_user_id VARCHAR(100) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(100), + company VARCHAR(100), + department VARCHAR(100), + global_role VARCHAR(20) DEFAULT 'USER', -- ADMIN, USER + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. 사용자별 앱 접근 권한 및 앱별 역할 +CREATE TABLE user_app_access ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + app_id INTEGER NOT NULL REFERENCES software_apps(id), + app_role VARCHAR(20) NOT NULL DEFAULT 'USER', -- SUPPORT, USER + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, app_id) +); + +-- 4. Q&A 상태 코드 +CREATE TABLE qna_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 5. Q&A 카테고리 코드 +CREATE TABLE qna_category_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 6. Q&A 게시글 +CREATE TABLE qna_posts ( + id SERIAL PRIMARY KEY, + app_id INTEGER NOT NULL REFERENCES software_apps(id), + user_id INTEGER NOT NULL REFERENCES users(id), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + category_code VARCHAR(20) REFERENCES qna_category_codes(code), + status_code VARCHAR(20) NOT NULL DEFAULT 'RECEIVED' REFERENCES qna_status_codes(code), + is_secret BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 7. 댓글 +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + post_id INTEGER NOT NULL REFERENCES qna_posts(id), + author_id INTEGER NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 8. 첨부파일 통합 관리 +CREATE TABLE attachments ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(20) NOT NULL, -- 'POST' 또는 'COMMENT' + parent_id INTEGER NOT NULL, + app_id INTEGER NOT NULL REFERENCES software_apps(id), + file_name VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 9. 원격 지원 상태 코드 +CREATE TABLE remote_support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 10. 원격 지원 로그 +CREATE TABLE remote_support ( + id SERIAL PRIMARY KEY, + post_id INTEGER NOT NULL REFERENCES qna_posts(id), + scheduled_time TIMESTAMP, + status_code VARCHAR(20) REFERENCES remote_support_status_codes(code), + support_engineer_id INTEGER REFERENCES users(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 11. 주요 조회 인덱스 +CREATE INDEX idx_user_app_access_app_role ON user_app_access (app_id, app_role); +CREATE INDEX idx_qna_posts_app_status_created ON qna_posts (app_id, status_code, created_at DESC); +CREATE INDEX idx_qna_posts_user_created ON qna_posts (user_id, created_at DESC); +CREATE INDEX idx_comments_post_created ON comments (post_id, created_at DESC); +CREATE INDEX idx_attachments_app_parent ON attachments (app_id, parent_type, parent_id); +``` + +## 4. 권한 및 보안 설계 + +### 4.1 역할 기반 접근 제어(RBAC) + +| 역할 | 접근 범위 | 주요 권한 | +| --- | --- | --- | +| Admin | 전사 전체 소프트웨어 | 전체 통계 조회, 모든 게시글 수정/삭제, FAQ 관리, 앱 권한 부여 | +| Support | 본인에게 할당된 소프트웨어 | 담당 앱 Q&A 답변, 원격 지원 시작, 상태 변경 | +| User | 본인이 사용 중인 소프트웨어 | Q&A 작성/조회, FAQ 검색, 원격 지원 신청 | + +### 4.2 권한 검증 로직 + +- 모든 API 요청에서 JWT 토큰 기준 사용자 식별 정보 추출 +- 전사 관리자 여부는 `users.global_role` 기준 확인 +- 앱 단위 접근 권한과 역할은 `user_app_access.app_role` 기준 검증 +- 요청 앱에 대한 권한이 없을 경우 `403 Forbidden` 반환 +- 비밀글(`is_secret = true`)의 작성자 본인, 권한 있는 Support, Admin 한정 조회 + +## 5. 주요 기능 및 UI 설계 + +### 5.1 통합 Q&A 리스트 + +- 제품별, 상태별, 날짜별 필터 제공 +- 접수중, 검토중, 패치예정, 해결완료 상태의 컬러 배지 구분을 통한 가시성 강화 +- MS Q&A 및 EG-BIM 사례를 참고한 검색성·가독성 중심 리스트 구조 적용 + +### 5.2 지능형 FAQ 및 원격 지원 + +- 질문 작성 시 제목 키워드 기반 관련 FAQ 실시간 추천 +- 게시글 내용이 복잡하거나 재현이 어려운 경우 담당자에 의한 원격 지원 세션 생성 및 링크 전달 +- Q&A에서 원격 지원으로 자연스럽게 전환되는 단순 운영 흐름 구성 + +### 5.3 파일 및 이미지 업로드 + +- 게시글 본문 및 댓글에서 드래그 앤 드롭 방식의 이미지 첨부 지원 +- `attachments` 테이블을 통한 게시글·댓글 출처 구분 및 보존·삭제 이력 관리 +- 초기 저장소의 로컬 볼륨 또는 S3 호환 스토리지 기준 검토 + +### 5.4 웹훅 기반 알림 연동 + +- ABC User Feedback에서 제공하는 웹훅을 활용한 앱별 Q&A 이벤트 수신 +- 각 앱 사용자의 Q&A 화면 접근 시 주요 공지 또는 신규 문의 현황 노출 +- 사용자의 신규 Q&A 글 작성 시 담당자 대상 네이버웍스 알림 발송 +- 담당자의 답변글 작성 시 작성자 대상 네이버웍스 알림 발송 +- 알림 이벤트의 앱별 라우팅, 수신 대상 매핑, 발송 이력 관리 체계 구성 + +## 6. 역할별 사용 시나리오 + +### 6.1 일반 사용자(User) 시나리오 + +- 각 소프트웨어 프로그램 로그인 +- Q&A 페이지 접근 +- 해당 앱 기준 Q&A 리스트 노출 및 기존 문의 확인 +- 신규 문의 글 작성 +- 담당자의 답변글 등록 시 네이버웍스 알림 수신 +- 알림 확인 후 Q&A 페이지에서 답변글 확인 + +```mermaid +flowchart LR + A[사용자 로그인] --> B[Q&A 페이지 접근] + B --> C[해당 앱 Q&A 리스트 확인] + C --> D[신규 문의 글 작성] + D --> E[담당자 답변 등록] + E --> F[네이버웍스 알림 수신] + F --> G[답변글 확인] +``` + +### 6.2 소프트웨어 담당자(Support) 시나리오 + +- 본인 담당 소프트웨어 관련 글만 노출 및 확인 +- 신규 등록 글의 새글 표시 확인 +- 문의 내용 검토 후 답변글 작성 +- 처리 단계에 따른 상태값 변경 +- 답변 등록 시 사용자 대상 네이버웍스 알림 발송 + +```mermaid +flowchart LR + A[담당 앱 글 목록 확인] --> B[새글 표시 확인] + B --> C[문의 내용 검토] + C --> D[답변글 작성] + D --> E[상태값 변경] + E --> F[사용자 대상 네이버웍스 알림 발송] +``` + +### 6.3 관리자(Admin) 시나리오 + +- 로컬 개발 환경에서 기능 개발 및 수정 +- Git 저장소 업로드 +- CI/CD 파이프라인을 통한 운영 서버 Docker 환경 배포 +- 배포 결과 및 운영 상태 확인 + +```mermaid +flowchart LR + A[로컬 개발 및 수정] --> B[Git 저장소 업로드] + B --> C[CI/CD 파이프라인 실행] + C --> D[운영 서버 Docker 배포] + D --> E[배포 결과 및 운영 상태 확인] +``` + +## 7. 구현 로드맵 + +### 7.1 Phase 1. 핵심 인프라 구축 + +- [ ] Ubuntu 및 Docker 서버 환경 셋업 +- [ ] BARON-SSO OAuth2 연동 및 사용자 매핑 로직 구현 +- [ ] RBAC 기반 DB 스키마 생성 및 기초 API 개발 + +### 7.2 Phase 2. 통합 플랫폼 UI 개발 + +- [ ] 소프트웨어별 격리 게시판 및 통합 리스트 UI 구현 +- [ ] 첨부파일 및 이미지 업로드 시스템 구축 +- [ ] 원격 지원 신청 기능 및 관리자 대시보드 연동 + +### 7.3 Phase 3. 고도화 및 운영 최적화 + +- [ ] AI 기반 FAQ 자동 추천 엔진 탑재 +- [ ] ABC User Feedback 웹훅 및 네이버웍스 알림 연동 고도화 +- [ ] 이슈 해결 통계 및 제품 품질 인사이트 보고서 자동화 + +### 7.4 추후 개발 예정 기능 + +#### 7.4.1 ADC User Feedback 연계 확장 기능 + +- 앱 메타정보 동기화 기능: BARON-SSO 또는 ADC User Feedback에 등록된 앱 코드, 앱명, 사용 여부 등의 정보를 우리 플랫폼과 자동으로 맞추는 기능 +- 상태 변경 이벤트 기반 네이버웍스 추가 알림 기능 +- FAQ, 기존 문의, 공지사항 기반 중복 문의 사전 방지 기능 +- 앱별 문의 건수, 처리량, 응답 시간 기준 통계 대시보드 기능 +- 소프트웨어별 Q&A 딥링크 또는 임베드 연동 기능: 각 소프트웨어 내부에서 해당 앱의 Q&A 화면으로 바로 이동하거나, Q&A 일부 화면을 프로그램 내부에 직접 표시하는 기능 + +#### 7.4.2 적용 검토 기준 + +- ADC User Feedback 제공 API, 웹훅, 임베드 기능의 실제 지원 범위 확인 +- SSO 앱 메타정보와 플랫폼 내부 운영 메타정보 간 동기화 가능 여부 확인 +- 네이버웍스 알림 대상자 매핑 및 부서별 알림 정책 적용 가능 여부 확인 +- 운영 복잡도 대비 활용 효과가 높은 기능 우선 적용 diff --git a/docs/관리페이지 md 파일/architecture_secretary.md b/docs/관리페이지 md 파일/architecture_secretary.md new file mode 100644 index 0000000..1edccf2 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary.md @@ -0,0 +1,517 @@ +# 사내 지원 플랫폼 상세 설계서 + +## 1. 프로젝트 개요 + +### 1.1 프로젝트 정보 + +| 항목 | 내용 | +| --- | --- | +| 플랫폼명 | BARON Office Support System | +| 핵심 목적 | 기존 통합 Q&A 플랫폼을 공용 기반으로 재사용하되, 인트라넷 진입형 사내 지원 서비스로 확장하여 물품신청, 도서 신청, 출장 차량 신청, 비품 대여, 사내 공지 및 Q&A 기능을 통합한 전사 지원 허브 구축 | +| 주요 대상 | S/W별 Q&A를 이용하는 사내 사용자 및 외부 고객(User), 인트라넷형 사내 지원 서비스를 이용하는 사내 사용자(User), 부서 담당자(Support), 시스템 관리자(Admin) | + +### 1.2 추진 배경 및 기대 효과 + +- 개별 메신저, 이메일, 구두 요청으로 분산된 사내 요청 채널의 단일 플랫폼 통합 +- 신청, 승인, 배정, 반납, 이력 조회의 전 과정 추적성 확보 +- BARON-SSO 기반 인증과 부서·서비스 단위 권한 제어를 통한 운영 효율 확보 +- 신청 현황, 처리 지연, 자산 이용률의 데이터 기반 관리 체계 확보 +- 기존 S/W별 Q&A 플랫폼과 동일한 공용 프레임워크 재사용을 통한 개발 및 운영 표준화 확보 +- 사내 사용자와 외부 고객을 함께 수용하는 사용자 모델 및 다중 알림 채널 운영 체계 확보 + +## 2. 기술 아키텍처 + +### 2.1 기술 스택 + +| 구분 | 기술 | +| --- | --- | +| Frontend | Next.js, React, Tailwind CSS, Headless UI | +| Backend | FastAPI, Python, SQLAlchemy, Pydantic | +| Database | PostgreSQL | +| 인증/권한 | BARON-SSO, OAuth 2.0, OpenID Connect | +| 외부 연동 | 네이버웍스 알림, SMS 발송 서비스, 사내 자산/사용자 정보 연동 API | +| 인프라 | Ubuntu 24.04 (WSL2), Docker Compose, Nginx | + +### 2.2 아키텍처 방향 + +- 프론트엔드의 Next.js 기반 구성으로 신청, 조회, 승인 화면의 일관된 사용자 경험 확보 +- 백엔드의 FastAPI 중심 API 구조 설계를 통한 신청, 승인, 자산관리, Q&A 기능 분리 구현 +- PostgreSQL과 SQLAlchemy 기반 데이터 계층 구성 및 요청 유형별 공통 처리 구조 반영 +- BARON-SSO 연동 전제의 인증 구조 적용 및 표준 OAuth 2.0 / OIDC 기반 세션·토큰 검증 수행 +- 신청 상태 변경과 승인 결과의 네이버웍스 실시간 알림 체계 확보 +- 네이버웍스 계정 미보유자 또는 발송 실패 건에 대한 SMS 대체 알림 체계 확보 +- 추후 서비스 유형 추가를 고려한 멀티서비스 확장형 모델 적용 +- 기존 S/W별 Q&A 플랫폼과 동일한 애플리케이션 기반을 재사용하되, 앱 진입형 구조 대신 인트라넷 진입형 서비스 컨텍스트 구조 적용 + +### 2.3 플랫폼 진입 구조 + +- 기존 기술지원 플랫폼은 각 S/W 프로그램 로그인 이후 해당 S/W의 Q&A 화면으로 직접 진입하는 앱 컨텍스트 기반 구조 적용 +- 사내 지원 플랫폼은 회사 인트라넷에서 진입한 뒤 서비스 유형별 메뉴를 선택하는 포털 진입형 구조 적용 +- 두 플랫폼은 동일한 인증 체계, 공통 UI 프레임워크, 공통 게시판/신청 처리 엔진을 공유하되 진입 URL과 초기 컨텍스트만 다르게 구성 +- 외부 진입 컨텍스트인 `app_id` 또는 `service_type_id`를 내부 공통 식별자인 `workspace_id`로 매핑하여 동일 DB 구조에서 처리 + +### 2.4 CI/CD 및 배포 프로세스 + +```mermaid +graph TD + A[개발자: 코드 작성 및 로컬 테스트] --> B[Main 브랜치 Push] + B --> C{GitHub Actions} + C --> D[Lint 및 Unit Test 실행] + D --> E[Docker Image 빌드] + E --> F[Container Registry 저장] + F --> G[운영 서버 배포] + G --> H[Nginx Proxy 라우팅] + H --> I[서비스 가동 및 모니터링] +``` + +## 3. 데이터베이스 설계 + +### 3.1 설계 원칙 + +기존 S/W별 Q&A 플랫폼과 인트라넷형 사내 지원 플랫폼을 동일 DB에서 운영하기 위해 모든 게시판·신청 데이터를 `workspace_id` 기준으로 통합 관리하도록 설계 + +- 외부 진입 채널은 달라도 내부 저장 구조는 공통 `workspace` 기준으로 통합 적용 +- S/W별 Q&A와 사내 지원 요청은 공통 본문·댓글·첨부 구조를 공유하고, 승인·자산배정·원격지원 등은 도메인별 확장 테이블로 분리 적용 +- 전역 관리자 권한과 워크스페이스별 운영 권한 분리를 통한 멀티플랫폼 확장 대응 +- 신규 S/W 또는 신규 사내 서비스 추가 시 공통 스키마 변경 없이 마스터 데이터 추가만으로 확장 가능한 구조 적용 +- 목록 조회 성능 확보를 위한 워크스페이스, 상태, 작성자 기준 인덱스 설계 반영 +- 사내 사용자와 외부 고객을 모두 지원할 수 있도록 사용자 유형과 다중 알림 채널 정보 관리 구조 반영 + +### 3.2 핵심 스키마 + +```sql +-- 1. 소프트웨어 정보 +CREATE TABLE software_apps ( + id SERIAL PRIMARY KEY, + app_code VARCHAR(50) UNIQUE NOT NULL, + app_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 사내 서비스 유형 정보 +CREATE TABLE service_types ( + id SERIAL PRIMARY KEY, + service_code VARCHAR(50) UNIQUE NOT NULL, + service_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. 사용자 정보 (BARON-SSO 동기화) +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + sso_user_id VARCHAR(100) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(100), + company VARCHAR(100), + department VARCHAR(100), + user_type VARCHAR(20) NOT NULL DEFAULT 'INTERNAL', -- INTERNAL, EXTERNAL + phone_number VARCHAR(30), + naverworks_user_key VARCHAR(100), + global_role VARCHAR(20) DEFAULT 'USER', -- ADMIN, USER + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 4. 공통 워크스페이스 마스터 +CREATE TABLE workspaces ( + id SERIAL PRIMARY KEY, + workspace_type VARCHAR(20) NOT NULL, -- SOFTWARE_APP, INTRANET_SERVICE + software_app_id INTEGER REFERENCES software_apps(id), + service_type_id INTEGER REFERENCES service_types(id), + workspace_code VARCHAR(50) UNIQUE NOT NULL, + workspace_name VARCHAR(100) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK ( + (workspace_type = 'SOFTWARE_APP' AND software_app_id IS NOT NULL AND service_type_id IS NULL) + OR + (workspace_type = 'INTRANET_SERVICE' AND service_type_id IS NOT NULL AND software_app_id IS NULL) + ) +); + +-- 5. 사용자별 워크스페이스 접근 권한 및 역할 +CREATE TABLE user_workspace_access ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + workspace_role VARCHAR(20) NOT NULL DEFAULT 'USER', -- SUPPORT, USER + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, workspace_id) +); + +-- 6. 공통 상태 코드 +CREATE TABLE support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 7. 공통 카테고리 코드 +CREATE TABLE support_category_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 8. 공통 게시글/신청 본문 +CREATE TABLE support_tickets ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + requester_id INTEGER NOT NULL REFERENCES users(id), + ticket_type VARCHAR(20) NOT NULL, -- QNA, REQUEST + title VARCHAR(255) NOT NULL, + content TEXT, + category_code VARCHAR(20) REFERENCES support_category_codes(code), + status_code VARCHAR(20) NOT NULL DEFAULT 'OPEN' REFERENCES support_status_codes(code), + is_secret BOOLEAN DEFAULT FALSE, + requested_start_at TIMESTAMP, + requested_end_at TIMESTAMP, + priority VARCHAR(20) DEFAULT 'NORMAL', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 9. 댓글 및 처리 메모 +CREATE TABLE ticket_comments ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + author_id INTEGER NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 10. 첨부파일 통합 관리 +CREATE TABLE attachments ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(20) NOT NULL, -- 'TICKET' 또는 'COMMENT' + parent_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + file_name VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 11. 사내 지원용 승인 이력 +CREATE TABLE request_approvals ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + approver_id INTEGER NOT NULL REFERENCES users(id), + approval_status VARCHAR(20) NOT NULL, -- APPROVED, REJECTED, PENDING + comment TEXT, + approved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 12. 자산/비품 마스터 +CREATE TABLE assets ( + id SERIAL PRIMARY KEY, + asset_code VARCHAR(50) UNIQUE NOT NULL, + asset_name VARCHAR(100) NOT NULL, + asset_type VARCHAR(30) NOT NULL, -- SUPPLY, EQUIPMENT, BOOK, VEHICLE + quantity INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT TRUE, + location VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 13. 자산 배정 및 대여 이력 +CREATE TABLE asset_allocations ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + assignee_id INTEGER REFERENCES users(id), + allocation_status VARCHAR(20) NOT NULL, -- RESERVED, LOANED, RETURNED, CANCELLED + loaned_at TIMESTAMP, + due_at TIMESTAMP, + returned_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 14. 차량 운행 일정 +CREATE TABLE vehicle_schedules ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + departure_at TIMESTAMP NOT NULL, + arrival_at TIMESTAMP, + destination VARCHAR(255), + driver_name VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 15. S/W Q&A용 원격 지원 상태 코드 +CREATE TABLE remote_support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 16. S/W Q&A용 원격 지원 로그 +CREATE TABLE remote_support ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + status_code VARCHAR(20) REFERENCES remote_support_status_codes(code), + support_engineer_id INTEGER REFERENCES users(id), + scheduled_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 17. 공통 FAQ +CREATE TABLE faqs ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 18. 알림 발송 이력 +CREATE TABLE notification_logs ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER REFERENCES support_tickets(id), + recipient_id INTEGER REFERENCES users(id), + channel VARCHAR(20) NOT NULL, -- NAVERWORKS, SMS + target_address VARCHAR(100), + delivery_status VARCHAR(20) NOT NULL, -- SUCCESS, FAILED, FALLBACK + fallback_channel VARCHAR(20), + error_message TEXT, + sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 19. 주요 조회 인덱스 +CREATE INDEX idx_user_workspace_access_role ON user_workspace_access (workspace_id, workspace_role); +CREATE INDEX idx_support_tickets_workspace_status_created ON support_tickets (workspace_id, status_code, created_at DESC); +CREATE INDEX idx_support_tickets_requester_created ON support_tickets (requester_id, created_at DESC); +CREATE INDEX idx_ticket_comments_ticket_created ON ticket_comments (ticket_id, created_at DESC); +CREATE INDEX idx_asset_allocations_asset_status ON asset_allocations (asset_id, allocation_status); +CREATE INDEX idx_vehicle_schedules_asset_departure ON vehicle_schedules (asset_id, departure_at); +CREATE INDEX idx_notification_logs_ticket_sent ON notification_logs (ticket_id, sent_at DESC); +``` + +### 3.3 워크스페이스 구성 예시 + +| 워크스페이스 유형 | 코드 | 이름 | 주요 처리 내용 | +| --- | --- | --- | --- | +| SOFTWARE_APP | EG_BIM | EG-BIM Q&A | 해당 S/W 전용 Q&A 게시판 및 원격 지원 | +| SOFTWARE_APP | CIVIL_APP | Civil App Q&A | 해당 S/W 전용 문의 접수 및 답변 | +| INTRANET_SERVICE | SUPPLY_REQUEST | 물품신청 | 사무용품, 소모품, 구매 요청 | +| INTRANET_SERVICE | BOOK_REQUEST | 도서 신청 | 업무 도서 구매 요청 또는 사내 도서 배정 | +| INTRANET_SERVICE | VEHICLE_REQUEST | 출장 차량 신청 | 출장 일정 기반 차량 예약 및 배차 요청 | +| INTRANET_SERVICE | EQUIPMENT_RENTAL | 비품 대여 | 노트북, 빔프로젝터, 케이블 등 비품 대여 | +| INTRANET_SERVICE | OFFICE_QNA | 사내 Q&A | 총무, 시설, 복지 관련 문의 접수 및 답변 | + +## 4. 권한 및 보안 설계 + +### 4.1 역할 기반 접근 제어(RBAC) + +| 역할 | 접근 범위 | 주요 권한 | +| --- | --- | --- | +| Admin | 전사 전체 워크스페이스 | 전체 통계 조회, 모든 게시글/신청 수정·삭제, 권한 부여, 워크스페이스 관리 | +| Support | 본인 담당 워크스페이스 | 신청 승인/반려, 배정 처리, 상태 변경, FAQ 관리, 답변 작성 | +| User | 본인이 접근 가능한 워크스페이스 | 신청 작성, 문의 작성, 본인 내역 조회, 댓글 작성, FAQ 조회 | + +### 4.2 권한 검증 로직 + +- 모든 API 요청에서 JWT 토큰 기준 사용자 식별 정보 추출 +- 전사 관리자 여부는 `users.global_role` 기준 확인 +- 워크스페이스 단위 접근 권한과 역할은 `user_workspace_access.workspace_role` 기준 검증 +- 요청 워크스페이스에 대한 권한이 없을 경우 `403 Forbidden` 반환 +- S/W별 Q&A 게시글과 사내 지원 신청은 모두 작성자 본인, 권한 있는 Support, Admin 한정 조회 +- S/W별 Q&A 워크스페이스는 사내 사용자(`INTERNAL`)와 외부 고객(`EXTERNAL`) 모두 접근 가능하도록 설계하고, 인트라넷형 서비스 워크스페이스는 사내 사용자 중심으로 제한 적용 + +## 5. 주요 기능 및 UI 설계 + +### 5.1 공통 워크스페이스 리스트 + +- 워크스페이스별, 상태별, 날짜별 필터 제공 +- S/W별 Q&A와 인트라넷형 사내 지원 서비스를 하나의 공통 리스트 엔진으로 구성 +- 사용자 진입 컨텍스트에 따라 해당 워크스페이스 화면만 우선 노출하는 구조 적용 +- 문의, 요청, 처리중, 완료, 반려 등 상태값의 컬러 배지 구분을 통한 가시성 강화 + +### 5.2 S/W별 Q&A 기능 + +- 각 S/W 로그인 이후 해당 앱 전용 Q&A 게시판으로 직접 진입하는 앱 컨텍스트 기반 게시판 기능 +- 제품별 문의 작성, 댓글 답변, 비밀글, 상태 변경 기능 +- 질문 작성 시 관련 FAQ 추천 및 기존 문의 검색 기능 +- 복잡한 문의에 대한 원격 지원 전환 및 지원 이력 관리 기능 +- 앱별 공지, 신규 문의 현황, 담당자 답변 상태 표시 기능 + +### 5.3 인트라넷형 사내 지원 기능 + +- 물품신청: 품목, 수량, 사용 목적 입력 기반 신청 기능 +- 도서 신청: 도서명, 저자, 출판사, 신청 사유 입력 기반 신청 기능 +- 출장 차량 신청: 출장 일정, 목적지, 탑승 인원 기반 차량 신청 기능 +- 비품 대여: 대여 품목, 사용 기간, 반납 예정일 입력 기반 대여 신청 기능 +- 사내 Q&A: 총무, 시설, 복지, 기타 운영 문의 작성 및 답변 기능 + +### 5.4 승인, 배정 및 원격 처리 + +- 사내 지원 요청에 대한 담당자 승인 또는 반려 처리 기능 +- 차량, 도서, 비품 등 실제 자산 배정 처리 기능 +- 반려 사유, 처리 메모, 배정 결과의 이력 관리 기능 +- 반납 완료 시 상태 자동 갱신 및 이력 보존 기능 +- S/W별 Q&A 문의에 대한 원격 지원 일정 등록 및 처리 상태 관리 기능 + +### 5.5 FAQ, 공지 및 추천 기능 + +- 워크스페이스별 FAQ 및 공지사항 분리 관리 기능 +- 질문 또는 신청 작성 시 관련 FAQ 및 기존 공지사항 추천 기능 +- 자주 발생하는 요청 유형과 반복 문의의 사전 안내를 통한 중복 문의 감소 기능 +- S/W별 앱 FAQ와 인트라넷형 서비스 안내 문서를 동일한 추천 구조로 제공 + +### 5.6 알림 및 이벤트 연동 + +- S/W별 Q&A 신규 문의 등록 시 담당자 대상 네이버웍스 알림 발송 +- S/W별 Q&A 답변 등록 시 작성자 대상 네이버웍스 알림 발송 +- 사내 지원 신청 등록 시 담당자 대상 네이버웍스 알림 발송 +- 승인, 반려, 배정, 반납 처리 시 신청자 대상 네이버웍스 알림 발송 +- 처리 지연 건 발생 시 담당자 리마인드 알림 발송 +- 워크스페이스 유형별 알림 대상 라우팅 및 발송 이력 관리 체계 구성 +- 네이버웍스 계정이 없는 사용자 또는 네이버웍스 발송 실패 건에 대한 SMS 대체 알림 발송 기능 +- 알림 발송 채널, 실패 사유, 대체 발송 결과를 `notification_logs` 기준으로 추적 관리 + +## 6. 역할별 사용 시나리오 + +### 6.1 S/W 사용자(User) 시나리오 + +- 각 S/W 프로그램 로그인 +- 사내 사용자 또는 외부 고객 자격으로 해당 앱 접근 +- 해당 앱의 Q&A 화면으로 직접 진입 +- 해당 앱 기준 Q&A 리스트 노출 및 기존 문의 확인 +- 신규 문의 글 작성 +- 담당자의 답변글 등록 시 네이버웍스 알림 수신 또는 SMS 대체 알림 수신 +- 알림 확인 후 해당 앱 Q&A 화면에서 답변글 확인 + +```mermaid +flowchart LR + A[사내 사용자 또는 외부 고객 로그인] --> B[해당 S/W Q&A 진입] + B --> C[해당 앱 Q&A 리스트 확인] + C --> D[신규 문의 글 작성] + D --> E[담당자 답변 등록] + E --> F[네이버웍스 또는 SMS 알림 수신] + F --> G[답변글 확인] +``` + +### 6.2 S/W 담당자(Support) 시나리오 + +- 본인 담당 S/W 워크스페이스 접근 +- 신규 문의 및 새글 표시 확인 +- 문의 내용 검토 후 답변글 작성 +- 상태값 변경 또는 원격 지원 전환 처리 +- 작성자 대상 알림 발송 + +```mermaid +flowchart LR + A[담당 S/W 워크스페이스 접근] --> B[신규 문의 확인] + B --> C[문의 내용 검토] + C --> D[답변 또는 원격 지원 처리] + D --> E[상태값 변경] + E --> F[사용자 대상 알림 발송] +``` + +### 6.3 인트라넷 사용자(User) 시나리오 + +- 회사 인트라넷 접속 및 BARON-SSO 로그인 +- 신청 서비스 선택 +- 물품신청, 도서 신청, 출장 차량 신청, 비품 대여 또는 사내 Q&A 작성 +- 처리 상태 및 승인 결과 확인 +- 네이버웍스 알림 수신 후 상세 내역 확인 + +```mermaid +flowchart LR + A[인트라넷 접속 및 로그인] --> B[신청 서비스 선택] + B --> C[신청서 또는 문의 작성] + C --> D[담당자 처리 진행] + D --> E[승인 또는 반려 결과 확정] + E --> F[네이버웍스 알림 수신] + F --> G[상세 내역 확인] +``` + +### 6.4 인트라넷 담당자(Support) 시나리오 + +- 본인 담당 서비스 워크스페이스 목록 확인 +- 신규 신청 또는 문의 접수 확인 +- 승인, 반려, 배정, 답변 처리 수행 +- 상태값 변경 및 처리 메모 등록 +- 신청자 대상 알림 발송 + +```mermaid +flowchart LR + A[담당 서비스 워크스페이스 확인] --> B[신규 요청 접수 확인] + B --> C[승인 또는 반려 검토] + C --> D[배정 또는 답변 처리] + D --> E[상태값 변경 및 메모 등록] + E --> F[신청자 대상 알림 발송] +``` + +### 6.5 관리자(Admin) 시나리오 + +- 로컬 개발 환경에서 기능 개발 및 수정 +- Git 저장소 업로드 +- CI/CD 파이프라인을 통한 운영 서버 Docker 환경 배포 +- S/W 앱 마스터, 서비스 유형, 워크스페이스 권한, 자산 마스터 관리 +- 배포 결과 및 운영 상태 확인 + +```mermaid +flowchart LR + A[로컬 개발 및 수정] --> B[Git 저장소 업로드] + B --> C[CI/CD 파이프라인 실행] + C --> D[운영 서버 Docker 배포] + D --> E[앱/서비스/권한/자산 마스터 관리] + E --> F[운영 상태 확인] +``` + +## 7. 구현 로드맵 + +### 7.1 Phase 1. 핵심 인프라 구축 + +- [ ] Ubuntu 및 Docker 서버 환경 셋업 +- [ ] BARON-SSO OAuth2 연동 및 사용자 매핑 로직 구현 +- [ ] 공용 워크스페이스 기반 DB 스키마 생성 및 기초 API 개발 +- [ ] S/W별 Q&A와 인트라넷형 서비스의 공통 인증 및 권한 구조 구현 + +### 7.2 Phase 2. S/W별 Q&A 및 공용 게시판 기능 개발 + +- [ ] S/W별 격리 게시판 및 통합 리스트 UI 구현 +- [ ] 댓글, 첨부파일, 비밀글, FAQ 추천 기능 구현 +- [ ] 원격 지원 전환 및 상태 관리 기능 구축 +- [ ] 네이버웍스 알림, SMS 대체 알림 및 앱별 컨텍스트 진입 기능 구현 + +### 7.3 Phase 3. 인트라넷형 사내 지원 기능 개발 + +- [ ] 서비스 유형별 신청서 UI 및 통합 리스트 구현 +- [ ] 승인/반려/배정 처리 기능 구현 +- [ ] 자산 및 차량 일정 관리 기능 구축 +- [ ] 첨부파일 및 댓글 시스템 구축 + +### 7.4 Phase 4. 고도화 및 운영 최적화 + +- [ ] FAQ 추천 및 중복 문의·중복 신청 사전 방지 기능 탑재 +- [ ] 네이버웍스 알림, SMS 대체 발송, 처리 지연 리마인드 기능 고도화 +- [ ] S/W별 Q&A 통계와 사내 지원 서비스 운영 인사이트 보고서 자동화 + +### 7.5 추후 개발 예정 기능 + +#### 7.5.1 공용 플랫폼 확장 기능 + +- 조직도 기반 결재선 자동 추천 기능: 신청 유형과 부서 기준으로 결재 대상 자동 추천 기능 +- 자산 메타정보 동기화 기능: 사내 자산관리 시스템 또는 외부 마스터 정보와 비품, 차량, 도서 정보를 자동 동기화하는 기능 +- 도서 및 비품 재고 예측 기능: 사용량 기반 부족 품목 예측 및 사전 구매 추천 기능 +- 서비스별 맞춤 대시보드 기능: 부서별 처리량, 반려율, 평균 승인 시간 시각화 기능 +- 앱 메타정보 동기화 기능: BARON-SSO 또는 외부 시스템에 등록된 S/W 정보를 공용 플랫폼과 자동으로 맞추는 기능 +- S/W별 Q&A 딥링크 또는 임베드 연동 기능: 각 소프트웨어 내부에서 해당 앱의 Q&A 화면으로 바로 이동하거나 일부 화면을 직접 표시하는 기능 +- 사내 포털 딥링크 또는 임베드 연동 기능: 그룹웨어 또는 사내 포털에서 해당 서비스 화면으로 바로 이동하거나 일부 화면을 직접 표시하는 기능 + +#### 7.5.2 적용 검토 기준 + +- 사내 자산관리, 그룹웨어, 조직도 API의 실제 연동 가능 범위 확인 +- BARON-SSO 또는 외부 시스템의 앱 메타정보 동기화 가능 범위 확인 +- 결재 정책과 운영 프로세스의 시스템 반영 가능 여부 확인 +- 네이버웍스 알림 대상자 및 부서별 알림 정책 적용 가능 여부 확인 +- 외부 고객 대상 SMS 발송 정책 및 개인정보 보관 기준 적용 가능 여부 확인 +- 운영 복잡도 대비 활용 효과가 높은 기능 우선 적용 diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso.md b/docs/관리페이지 md 파일/architecture_secretary_sso.md new file mode 100644 index 0000000..91ec620 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso.md @@ -0,0 +1,527 @@ +# 사내 지원 플랫폼 상세 설계서 + +## 1. 프로젝트 개요 + +### 1.1 프로젝트 정보 + +| 항목 | 내용 | +| --- | --- | +| 플랫폼명 | BARON Office Support System | +| 핵심 목적 | 기존 통합 Q&A 플랫폼을 공용 기반으로 재사용하되, 인트라넷 진입형 사내 지원 서비스로 확장하여 물품신청, 도서 신청, 출장 차량 신청, 비품 대여, 사내 공지 및 Q&A 기능을 통합한 전사 지원 허브 구축 | +| 주요 대상 | S/W별 Q&A를 이용하는 사내 사용자 및 외부 고객(User), 인트라넷형 사내 지원 서비스를 이용하는 사내 사용자(User), 부서 담당자(Support), 시스템 관리자(Admin) | + +### 1.2 추진 배경 및 기대 효과 + +- 개별 메신저, 이메일, 구두 요청으로 분산된 사내 요청 채널의 단일 플랫폼 통합 +- 신청, 승인, 배정, 반납, 이력 조회의 전 과정 추적성 확보 +- BARON-SSO 기반 인증과 부서·서비스 단위 권한 제어를 통한 운영 효율 확보 +- 신청 현황, 처리 지연, 자산 이용률의 데이터 기반 관리 체계 확보 +- 기존 S/W별 Q&A 플랫폼과 동일한 공용 프레임워크 재사용을 통한 개발 및 운영 표준화 확보 +- 사내 사용자와 외부 고객을 함께 수용하는 사용자 모델 및 다중 알림 채널 운영 체계 확보 + +## 2. 기술 아키텍처 + +### 2.1 기술 스택 + +| 구분 | 기술 | +| --- | --- | +| Frontend | Next.js, React, Tailwind CSS, Headless UI | +| Backend | FastAPI, Python, SQLAlchemy, Pydantic | +| Database | PostgreSQL | +| 인증/권한 | BARON-SSO, OAuth 2.0, OpenID Connect | +| 외부 연동 | 네이버웍스 알림, SMS 발송 서비스, 사내 자산/사용자 정보 연동 API | +| 인프라 | Ubuntu 24.04 (WSL2), Docker Compose, Nginx | + +### 2.2 아키텍처 방향 + +- 프론트엔드의 Next.js 기반 구성으로 신청, 조회, 승인 화면의 일관된 사용자 경험 확보 +- 백엔드의 FastAPI 중심 API 구조 설계를 통한 신청, 승인, 자산관리, Q&A 기능 분리 구현 +- PostgreSQL과 SQLAlchemy 기반 데이터 계층 구성 및 요청 유형별 공통 처리 구조 반영 +- BARON-SSO 연동 전제의 인증 구조 적용 및 표준 OAuth 2.0 / OIDC 기반 세션·토큰 검증 수행 +- 신청 상태 변경과 승인 결과의 네이버웍스 실시간 알림 체계 확보 +- 네이버웍스 계정 미보유자 또는 발송 실패 건에 대한 SMS 대체 알림 체계 확보 +- 추후 서비스 유형 추가를 고려한 멀티서비스 확장형 모델 적용 +- 기존 S/W별 Q&A 플랫폼과 동일한 애플리케이션 기반을 재사용하되, 앱 진입형 구조 대신 인트라넷 진입형 서비스 컨텍스트 구조 적용 + +### 2.3 플랫폼 진입 구조 + +- 기존 기술지원 플랫폼은 각 S/W 프로그램 로그인 이후 해당 S/W의 Q&A 화면으로 직접 진입하는 앱 컨텍스트 기반 구조 적용 +- 사내 지원 플랫폼은 회사 인트라넷에서 진입한 뒤 서비스 유형별 메뉴를 선택하는 포털 진입형 구조 적용 +- 두 플랫폼은 동일한 인증 체계, 공통 UI 프레임워크, 공통 게시판/신청 처리 엔진을 공유하되 진입 URL과 초기 컨텍스트만 다르게 구성 +- 외부 진입 컨텍스트인 `app_id` 또는 `service_type_id`를 내부 공통 식별자인 `workspace_id`로 매핑하여 동일 DB 구조에서 처리 + +### 2.4 CI/CD 및 배포 프로세스 + +```mermaid +graph TD + A[개발자: 코드 작성 및 로컬 테스트] --> B[Main 브랜치 Push] + B --> C{GitHub Actions} + C --> D[Lint 및 Unit Test 실행] + D --> E[Docker Image 빌드] + E --> F[Container Registry 저장] + F --> G[운영 서버 배포] + G --> H[Nginx Proxy 라우팅] + H --> I[서비스 가동 및 모니터링] +``` + +## 3. 데이터베이스 설계 + +### 3.1 설계 원칙 + +기존 S/W별 Q&A 플랫폼과 인트라넷형 사내 지원 플랫폼을 동일 DB에서 운영하기 위해 모든 게시판·신청 데이터를 `workspace_id` 기준으로 통합 관리하도록 설계 + +- 외부 진입 채널은 달라도 내부 저장 구조는 공통 `workspace` 기준으로 통합 적용 +- S/W별 Q&A와 사내 지원 요청은 공통 본문·댓글·첨부 구조를 공유하고, 승인·자산배정·원격지원 등은 도메인별 확장 테이블로 분리 적용 +- 전역 관리자 권한과 워크스페이스별 운영 권한 분리를 통한 멀티플랫폼 확장 대응 +- 신규 S/W 또는 신규 사내 서비스 추가 시 공통 스키마 변경 없이 마스터 데이터 추가만으로 확장 가능한 구조 적용 +- 목록 조회 성능 확보를 위한 워크스페이스, 상태, 작성자 기준 인덱스 설계 반영 +- 사용자 기본 정보와 tenant 정보는 BARON-SSO를 원본으로 사용하고, 플랫폼 내부에서는 `user_id`, `tenant_id` 및 권한 정보만 관리하는 구조 반영 + +### 3.2 핵심 스키마 + +```sql +-- 1. 소프트웨어 정보 +CREATE TABLE software_apps ( + id SERIAL PRIMARY KEY, + app_code VARCHAR(50) UNIQUE NOT NULL, + app_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 사내 서비스 유형 정보 +CREATE TABLE service_types ( + id SERIAL PRIMARY KEY, + service_code VARCHAR(50) UNIQUE NOT NULL, + service_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. 공통 워크스페이스 마스터 +CREATE TABLE workspaces ( + id SERIAL PRIMARY KEY, + workspace_type VARCHAR(20) NOT NULL, -- SOFTWARE_APP, INTRANET_SERVICE + software_app_id INTEGER REFERENCES software_apps(id), + service_type_id INTEGER REFERENCES service_types(id), + workspace_code VARCHAR(50) UNIQUE NOT NULL, + workspace_name VARCHAR(100) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK ( + (workspace_type = 'SOFTWARE_APP' AND software_app_id IS NOT NULL AND service_type_id IS NULL) + OR + (workspace_type = 'INTRANET_SERVICE' AND service_type_id IS NOT NULL AND software_app_id IS NULL) + ) +); + +-- 4. 사용자별 워크스페이스 접근 권한 및 역할 +CREATE TABLE user_workspace_access ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + workspace_role VARCHAR(20) NOT NULL DEFAULT 'USER', -- SUPPORT, USER + can_read BOOLEAN DEFAULT TRUE, + can_write BOOLEAN DEFAULT FALSE, + can_manage BOOLEAN DEFAULT FALSE, + can_approve BOOLEAN DEFAULT FALSE, + page_scope VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, tenant_id, workspace_id) +); + +-- 5. 사용자 알림 보조 정보 +CREATE TABLE user_notification_profiles ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + user_type VARCHAR(20) NOT NULL DEFAULT 'INTERNAL', -- INTERNAL, EXTERNAL + phone_number VARCHAR(30), + naverworks_user_key VARCHAR(100), + sms_opt_in BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, tenant_id) +); + +-- 6. 공통 상태 코드 +CREATE TABLE support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 7. 공통 카테고리 코드 +CREATE TABLE support_category_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 8. 공통 게시글/신청 본문 +CREATE TABLE support_tickets ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + requester_id VARCHAR(100) NOT NULL, + requester_tenant_id VARCHAR(100) NOT NULL, + ticket_type VARCHAR(20) NOT NULL, -- QNA, REQUEST + title VARCHAR(255) NOT NULL, + content TEXT, + category_code VARCHAR(20) REFERENCES support_category_codes(code), + status_code VARCHAR(20) NOT NULL DEFAULT 'OPEN' REFERENCES support_status_codes(code), + is_secret BOOLEAN DEFAULT FALSE, + requested_start_at TIMESTAMP, + requested_end_at TIMESTAMP, + priority VARCHAR(20) DEFAULT 'NORMAL', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 9. 댓글 및 처리 메모 +CREATE TABLE ticket_comments ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + author_id VARCHAR(100) NOT NULL, + author_tenant_id VARCHAR(100) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 10. 첨부파일 통합 관리 +CREATE TABLE attachments ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(20) NOT NULL, -- 'TICKET' 또는 'COMMENT' + parent_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + file_name VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 11. 사내 지원용 승인 이력 +CREATE TABLE request_approvals ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + approver_id VARCHAR(100) NOT NULL, + approver_tenant_id VARCHAR(100) NOT NULL, + approval_status VARCHAR(20) NOT NULL, -- APPROVED, REJECTED, PENDING + comment TEXT, + approved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 12. 자산/비품 마스터 +CREATE TABLE assets ( + id SERIAL PRIMARY KEY, + asset_code VARCHAR(50) UNIQUE NOT NULL, + asset_name VARCHAR(100) NOT NULL, + asset_type VARCHAR(30) NOT NULL, -- SUPPLY, EQUIPMENT, BOOK, VEHICLE + quantity INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT TRUE, + location VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 13. 자산 배정 및 대여 이력 +CREATE TABLE asset_allocations ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + assignee_id VARCHAR(100), + assignee_tenant_id VARCHAR(100), + allocation_status VARCHAR(20) NOT NULL, -- RESERVED, LOANED, RETURNED, CANCELLED + loaned_at TIMESTAMP, + due_at TIMESTAMP, + returned_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 14. 차량 운행 일정 +CREATE TABLE vehicle_schedules ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + departure_at TIMESTAMP NOT NULL, + arrival_at TIMESTAMP, + destination VARCHAR(255), + driver_name VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 15. S/W Q&A용 원격 지원 상태 코드 +CREATE TABLE remote_support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 16. S/W Q&A용 원격 지원 로그 +CREATE TABLE remote_support ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + status_code VARCHAR(20) REFERENCES remote_support_status_codes(code), + support_engineer_id VARCHAR(100), + support_engineer_tenant_id VARCHAR(100), + scheduled_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 17. 공통 FAQ +CREATE TABLE faqs ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 18. 알림 발송 이력 +CREATE TABLE notification_logs ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER REFERENCES support_tickets(id), + recipient_id VARCHAR(100) NOT NULL, + recipient_tenant_id VARCHAR(100) NOT NULL, + channel VARCHAR(20) NOT NULL, -- NAVERWORKS, SMS + target_address VARCHAR(100), + delivery_status VARCHAR(20) NOT NULL, -- SUCCESS, FAILED, FALLBACK + fallback_channel VARCHAR(20), + error_message TEXT, + sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 19. 주요 조회 인덱스 +CREATE INDEX idx_user_workspace_access_role ON user_workspace_access (workspace_id, workspace_role); +CREATE INDEX idx_support_tickets_workspace_status_created ON support_tickets (workspace_id, status_code, created_at DESC); +CREATE INDEX idx_support_tickets_requester_created ON support_tickets (requester_id, created_at DESC); +CREATE INDEX idx_ticket_comments_ticket_created ON ticket_comments (ticket_id, created_at DESC); +CREATE INDEX idx_asset_allocations_asset_status ON asset_allocations (asset_id, allocation_status); +CREATE INDEX idx_vehicle_schedules_asset_departure ON vehicle_schedules (asset_id, departure_at); +CREATE INDEX idx_notification_logs_ticket_sent ON notification_logs (ticket_id, sent_at DESC); +``` + +### 3.3 워크스페이스 구성 예시 + +| 워크스페이스 유형 | 코드 | 이름 | 주요 처리 내용 | +| --- | --- | --- | --- | +| SOFTWARE_APP | EG_BIM | EG-BIM Q&A | 해당 S/W 전용 Q&A 게시판 및 원격 지원 | +| SOFTWARE_APP | CIVIL_APP | Civil App Q&A | 해당 S/W 전용 문의 접수 및 답변 | +| INTRANET_SERVICE | SUPPLY_REQUEST | 물품신청 | 사무용품, 소모품, 구매 요청 | +| INTRANET_SERVICE | BOOK_REQUEST | 도서 신청 | 업무 도서 구매 요청 또는 사내 도서 배정 | +| INTRANET_SERVICE | VEHICLE_REQUEST | 출장 차량 신청 | 출장 일정 기반 차량 예약 및 배차 요청 | +| INTRANET_SERVICE | EQUIPMENT_RENTAL | 비품 대여 | 노트북, 빔프로젝터, 케이블 등 비품 대여 | +| INTRANET_SERVICE | OFFICE_QNA | 사내 Q&A | 총무, 시설, 복지 관련 문의 접수 및 답변 | + +## 4. 권한 및 보안 설계 + +### 4.1 역할 기반 접근 제어(RBAC) + +| 역할 | 접근 범위 | 주요 권한 | +| --- | --- | --- | +| Admin | 전사 전체 워크스페이스 | 전체 통계 조회, 모든 게시글/신청 수정·삭제, 권한 부여, 워크스페이스 관리 | +| Support | 본인 담당 워크스페이스 | 신청 승인/반려, 배정 처리, 상태 변경, FAQ 관리, 답변 작성 | +| User | 본인이 접근 가능한 워크스페이스 | 신청 작성, 문의 작성, 본인 내역 조회, 댓글 작성, FAQ 조회 | + +### 4.2 권한 검증 로직 + +- 모든 API 요청에서 BARON-SSO 토큰 기준 `user_id`, `tenant_id` 추출 +- 전사 관리자 여부와 사용자 기본 정보는 BARON-SSO 기준 확인 +- 워크스페이스 단위 접근 권한과 역할은 `user_workspace_access`의 `can_read`, `can_write`, `can_manage`, `can_approve`, `page_scope` 기준 검증 +- 요청 워크스페이스에 대한 권한이 없을 경우 `403 Forbidden` 반환 +- S/W별 Q&A 게시글과 사내 지원 신청은 모두 작성자 본인, 권한 있는 Support, Admin 한정 조회 +- S/W별 Q&A 워크스페이스는 사내 사용자(`INTERNAL`)와 외부 고객(`EXTERNAL`) 모두 접근 가능하도록 설계하고, 인트라넷형 서비스 워크스페이스는 사내 사용자 중심으로 제한 적용 + +## 5. 주요 기능 및 UI 설계 + +### 5.1 공통 워크스페이스 리스트 + +- 워크스페이스별, 상태별, 날짜별 필터 제공 +- S/W별 Q&A와 인트라넷형 사내 지원 서비스를 하나의 공통 리스트 엔진으로 구성 +- 사용자 진입 컨텍스트에 따라 해당 워크스페이스 화면만 우선 노출하는 구조 적용 +- 문의, 요청, 처리중, 완료, 반려 등 상태값의 컬러 배지 구분을 통한 가시성 강화 + +### 5.2 S/W별 Q&A 기능 + +- 각 S/W 로그인 이후 해당 앱 전용 Q&A 게시판으로 직접 진입하는 앱 컨텍스트 기반 게시판 기능 +- 제품별 문의 작성, 댓글 답변, 비밀글, 상태 변경 기능 +- 질문 작성 시 관련 FAQ 추천 및 기존 문의 검색 기능 +- 복잡한 문의에 대한 원격 지원 전환 및 지원 이력 관리 기능 +- 앱별 공지, 신규 문의 현황, 담당자 답변 상태 표시 기능 + +### 5.3 인트라넷형 사내 지원 기능 + +- 물품신청: 품목, 수량, 사용 목적 입력 기반 신청 기능 +- 도서 신청: 도서명, 저자, 출판사, 신청 사유 입력 기반 신청 기능 +- 출장 차량 신청: 출장 일정, 목적지, 탑승 인원 기반 차량 신청 기능 +- 비품 대여: 대여 품목, 사용 기간, 반납 예정일 입력 기반 대여 신청 기능 +- 사내 Q&A: 총무, 시설, 복지, 기타 운영 문의 작성 및 답변 기능 + +### 5.4 승인, 배정 및 원격 처리 + +- 사내 지원 요청에 대한 담당자 승인 또는 반려 처리 기능 +- 차량, 도서, 비품 등 실제 자산 배정 처리 기능 +- 반려 사유, 처리 메모, 배정 결과의 이력 관리 기능 +- 반납 완료 시 상태 자동 갱신 및 이력 보존 기능 +- S/W별 Q&A 문의에 대한 원격 지원 일정 등록 및 처리 상태 관리 기능 + +### 5.5 FAQ, 공지 및 추천 기능 + +- 워크스페이스별 FAQ 및 공지사항 분리 관리 기능 +- 질문 또는 신청 작성 시 관련 FAQ 및 기존 공지사항 추천 기능 +- 자주 발생하는 요청 유형과 반복 문의의 사전 안내를 통한 중복 문의 감소 기능 +- S/W별 앱 FAQ와 인트라넷형 서비스 안내 문서를 동일한 추천 구조로 제공 + +### 5.6 알림 및 이벤트 연동 + +- S/W별 Q&A 신규 문의 등록 시 담당자 대상 네이버웍스 알림 발송 +- S/W별 Q&A 답변 등록 시 작성자 대상 네이버웍스 알림 발송 +- 사내 지원 신청 등록 시 담당자 대상 네이버웍스 알림 발송 +- 승인, 반려, 배정, 반납 처리 시 신청자 대상 네이버웍스 알림 발송 +- 처리 지연 건 발생 시 담당자 리마인드 알림 발송 +- 워크스페이스 유형별 알림 대상 라우팅 및 발송 이력 관리 체계 구성 +- 네이버웍스 계정이 없는 사용자 또는 네이버웍스 발송 실패 건에 대한 SMS 대체 알림 발송 기능 +- 알림 발송 채널, 실패 사유, 대체 발송 결과를 `notification_logs` 기준으로 추적 관리 + +## 6. 역할별 사용 시나리오 + +### 6.1 S/W 사용자(User) 시나리오 + +- 각 S/W 프로그램 로그인 +- 사내 사용자 또는 외부 고객 자격으로 해당 앱 접근 +- 해당 앱의 Q&A 화면으로 직접 진입 +- 해당 앱 기준 Q&A 리스트 노출 및 기존 문의 확인 +- 신규 문의 글 작성 +- 담당자의 답변글 등록 시 네이버웍스 알림 수신 또는 SMS 대체 알림 수신 +- 알림 확인 후 해당 앱 Q&A 화면에서 답변글 확인 + +```mermaid +flowchart LR + A[사내 사용자 또는 외부 고객 로그인] --> B[해당 S/W Q&A 진입] + B --> C[해당 앱 Q&A 리스트 확인] + C --> D[신규 문의 글 작성] + D --> E[담당자 답변 등록] + E --> F[네이버웍스 또는 SMS 알림 수신] + F --> G[답변글 확인] +``` + +### 6.2 S/W 담당자(Support) 시나리오 + +- 본인 담당 S/W 워크스페이스 접근 +- 신규 문의 및 새글 표시 확인 +- 문의 내용 검토 후 답변글 작성 +- 상태값 변경 또는 원격 지원 전환 처리 +- 작성자 대상 알림 발송 + +```mermaid +flowchart LR + A[담당 S/W 워크스페이스 접근] --> B[신규 문의 확인] + B --> C[문의 내용 검토] + C --> D[답변 또는 원격 지원 처리] + D --> E[상태값 변경] + E --> F[사용자 대상 알림 발송] +``` + +### 6.3 인트라넷 사용자(User) 시나리오 + +- 회사 인트라넷 접속 및 BARON-SSO 로그인 +- 신청 서비스 선택 +- 물품신청, 도서 신청, 출장 차량 신청, 비품 대여 또는 사내 Q&A 작성 +- 처리 상태 및 승인 결과 확인 +- 네이버웍스 알림 수신 후 상세 내역 확인 + +```mermaid +flowchart LR + A[인트라넷 접속 및 로그인] --> B[신청 서비스 선택] + B --> C[신청서 또는 문의 작성] + C --> D[담당자 처리 진행] + D --> E[승인 또는 반려 결과 확정] + E --> F[네이버웍스 알림 수신] + F --> G[상세 내역 확인] +``` + +### 6.4 인트라넷 담당자(Support) 시나리오 + +- 본인 담당 서비스 워크스페이스 목록 확인 +- 신규 신청 또는 문의 접수 확인 +- 승인, 반려, 배정, 답변 처리 수행 +- 상태값 변경 및 처리 메모 등록 +- 신청자 대상 알림 발송 + +```mermaid +flowchart LR + A[담당 서비스 워크스페이스 확인] --> B[신규 요청 접수 확인] + B --> C[승인 또는 반려 검토] + C --> D[배정 또는 답변 처리] + D --> E[상태값 변경 및 메모 등록] + E --> F[신청자 대상 알림 발송] +``` + +### 6.5 관리자(Admin) 시나리오 + +- 로컬 개발 환경에서 기능 개발 및 수정 +- Git 저장소 업로드 +- CI/CD 파이프라인을 통한 운영 서버 Docker 환경 배포 +- S/W 앱 마스터, 서비스 유형, 워크스페이스 권한, 자산 마스터 관리 +- 배포 결과 및 운영 상태 확인 + +```mermaid +flowchart LR + A[로컬 개발 및 수정] --> B[Git 저장소 업로드] + B --> C[CI/CD 파이프라인 실행] + C --> D[운영 서버 Docker 배포] + D --> E[앱/서비스/권한/자산 마스터 관리] + E --> F[운영 상태 확인] +``` + +## 7. 구현 로드맵 + +### 7.1 Phase 1. 핵심 인프라 구축 + +- [ ] Ubuntu 및 Docker 서버 환경 셋업 +- [ ] BARON-SSO OAuth2 연동 및 사용자 매핑 로직 구현 +- [ ] 공용 워크스페이스 기반 DB 스키마 생성 및 기초 API 개발 +- [ ] S/W별 Q&A와 인트라넷형 서비스의 공통 인증 및 권한 구조 구현 + +### 7.2 Phase 2. S/W별 Q&A 및 공용 게시판 기능 개발 + +- [ ] S/W별 격리 게시판 및 통합 리스트 UI 구현 +- [ ] 댓글, 첨부파일, 비밀글, FAQ 추천 기능 구현 +- [ ] 원격 지원 전환 및 상태 관리 기능 구축 +- [ ] 네이버웍스 알림, SMS 대체 알림 및 앱별 컨텍스트 진입 기능 구현 + +### 7.3 Phase 3. 인트라넷형 사내 지원 기능 개발 + +- [ ] 서비스 유형별 신청서 UI 및 통합 리스트 구현 +- [ ] 승인/반려/배정 처리 기능 구현 +- [ ] 자산 및 차량 일정 관리 기능 구축 +- [ ] 첨부파일 및 댓글 시스템 구축 + +### 7.4 Phase 4. 고도화 및 운영 최적화 + +- [ ] FAQ 추천 및 중복 문의·중복 신청 사전 방지 기능 탑재 +- [ ] 네이버웍스 알림, SMS 대체 발송, 처리 지연 리마인드 기능 고도화 +- [ ] S/W별 Q&A 통계와 사내 지원 서비스 운영 인사이트 보고서 자동화 + +### 7.5 추후 개발 예정 기능 + +#### 7.5.1 공용 플랫폼 확장 기능 + +- 조직도 기반 결재선 자동 추천 기능: 신청 유형과 부서 기준으로 결재 대상 자동 추천 기능 +- 자산 메타정보 동기화 기능: 사내 자산관리 시스템 또는 외부 마스터 정보와 비품, 차량, 도서 정보를 자동 동기화하는 기능 +- 도서 및 비품 재고 예측 기능: 사용량 기반 부족 품목 예측 및 사전 구매 추천 기능 +- 서비스별 맞춤 대시보드 기능: 부서별 처리량, 반려율, 평균 승인 시간 시각화 기능 +- 앱 메타정보 동기화 기능: BARON-SSO 또는 외부 시스템에 등록된 S/W 정보를 공용 플랫폼과 자동으로 맞추는 기능 +- S/W별 Q&A 딥링크 또는 임베드 연동 기능: 각 소프트웨어 내부에서 해당 앱의 Q&A 화면으로 바로 이동하거나 일부 화면을 직접 표시하는 기능 +- 사내 포털 딥링크 또는 임베드 연동 기능: 그룹웨어 또는 사내 포털에서 해당 서비스 화면으로 바로 이동하거나 일부 화면을 직접 표시하는 기능 + +#### 7.5.2 적용 검토 기준 + +- 사내 자산관리, 그룹웨어, 조직도 API의 실제 연동 가능 범위 확인 +- BARON-SSO 또는 외부 시스템의 앱 메타정보 동기화 가능 범위 확인 +- 결재 정책과 운영 프로세스의 시스템 반영 가능 여부 확인 +- 네이버웍스 알림 대상자 및 부서별 알림 정책 적용 가능 여부 확인 +- 외부 고객 대상 SMS 발송 정책 및 개인정보 보관 기준 적용 가능 여부 확인 +- 운영 복잡도 대비 활용 효과가 높은 기능 우선 적용 diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_components.md b/docs/관리페이지 md 파일/architecture_secretary_sso_components.md new file mode 100644 index 0000000..47a5184 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_components.md @@ -0,0 +1,497 @@ +# 사내 지원 플랫폼 통합 아키텍처 설계서 + +## 1. 문서 목적 + +본 문서는 BARON-SSO 기반 공용 플랫폼으로 다음 두 서비스를 하나의 구조로 통합하는 방안을 설명함. + +- 각 S/W 프로그램에서 진입하는 S/W별 Q&A 플랫폼 +- 회사 인트라넷에서 진입하는 사내 지원 플랫폼 + +본 문서는 상세 테이블 설명보다 먼저 서비스의 전체 그림, 사용자 진입 방식, 인증 구조, 핵심 처리 계층, 외부 연동 방식을 이해할 수 있도록 정리함. + +## 2. 한눈에 보는 서비스 구조 + +### 2.1 통합 대상 서비스 + +| 구분 | 설명 | +| --- | --- | +| S/W Q&A | 각 S/W 프로그램에서 로그인 후 해당 앱 전용 Q&A 게시판으로 연결되는 지원 서비스 | +| 사내 지원 | 인트라넷에서 진입하여 물품신청, 도서 신청, 출장 차량 신청, 비품 대여, 사내 Q&A를 처리하는 지원 서비스 | + +### 2.2 핵심 설계 판단 + +- 사용자와 tenant의 원본 정보는 BARON-SSO에서 관리 +- 플랫폼 내부에서는 사용자 마스터를 별도로 두지 않고 `user_id`, `tenant_id` 기반으로 권한만 관리 +- 두 서비스는 각각 별도 시스템으로 만들지 않고 공통 `workspace` 기반 플랫폼으로 통합 +- 게시글, 신청, 댓글, 첨부, FAQ, 알림은 공통 엔진으로 처리 +- 승인, 자산, 차량, 원격지원은 서비스별 확장 기능으로 분리 +- 승인 및 반려는 일반 사용자 화면이 아니라 담당자용 운영 페이지에서 처리 + +## 3. High-Level Architecture + +### 3.1 아키텍처 관점 + +| 관점 | 설명 | +| --- | --- | +| 사용자 접점 | 사용자가 어디서 진입하는지 | +| 인증 계층 | BARON-SSO가 어디에서 인증을 담당하는지 | +| 핵심 서비스 | 어떤 애플리케이션 계층에서 업무를 처리하는지 | +| 인프라 및 외부 연동 | 데이터 저장과 알림, 외부 시스템 연동이 어디서 발생하는지 | + +### 3.2 High-Level Architecture 다이어그램 + +```mermaid +flowchart LR + subgraph U[사용자 영역] + U1[인트라넷 사용자] + U2[사내 S/W 사용자] + U3[외부 고객] + end + + subgraph E[사용자 접점] + E1[인트라넷 포털] + E2[각 S/W 프로그램] + end + + subgraph A[인증 계층] + A1[BARON-SSO\nOAuth 2.0 / OIDC] + end + + subgraph P[플랫폼 시스템] + subgraph T1[UI Tier] + F1[웹 클라이언트\nNext.js] + F2[운영 / 관리 페이지\n담당자 · 승인자 · 관리자] + end + + subgraph T2[API Tier] + B1[지원 플랫폼 API\nFastAPI] + B2[권한 제어\nuser_id, tenant_id, workspace 기반] + B3[업무 처리 엔진\nQ&A / 신청 / 승인 / 자산 / 차량 / 원격지원] + B4[알림 처리\nNaver Works 우선, SMS 대체] + end + + subgraph T3[Data Tier] + D1[PostgreSQL] + end + end + + subgraph X[외부 연동] + X1[Naver Works API] + X2[SMS Gateway] + X3[자산 / 조직도 / 기타 사내 API] + end + + U1 --> E1 + U2 --> E2 + U3 --> E2 + E1 --> F1 + E2 --> F1 + F1 --> A1 + A1 --> F1 + F1 --> B1 + F2 --> A1 + A1 --> F2 + F2 --> B1 + B1 --> B2 + B1 --> B3 + B2 --> D1 + B3 --> D1 + B1 --> B4 + B4 --> X1 + B4 --> X2 + B3 --> X3 +``` + +### 3.3 전체 흐름 요약 + +1. 사용자는 인트라넷 포털 또는 각 S/W 프로그램에서 지원 플랫폼으로 진입함. +2. 웹 클라이언트는 BARON-SSO를 통해 인증을 수행하고 `user_id`, `tenant_id`를 확보함. +3. 플랫폼 API는 진입 경로의 `app_id` 또는 `service_type_id`를 내부 `workspace`로 매핑함. +4. 권한 제어 계층은 사용자별 읽기, 쓰기, 관리, 승인 범위를 확인함. +5. 일반 사용자는 사용자 화면에서 문의 또는 신청을 등록하고, 담당자는 운영 페이지에서 승인, 반려, 답변, 상태 변경을 처리함. +6. 데이터는 PostgreSQL에 저장하고 알림은 네이버웍스 우선, 실패 시 SMS로 대체 발송함. +7. 필요 시 자산 시스템, 조직도, 기타 사내 API와 연계함. + +## 4. 서비스 구성 + +### 4.1 공통 플랫폼으로 통합하는 이유 + +두 서비스는 진입 채널과 세부 기능은 다르지만, 실제로는 다음 기능을 공통으로 사용함. + +- 게시글 또는 신청서 작성 +- 담당자 답변 및 처리 이력 관리 +- 첨부파일 관리 +- FAQ 및 공지 제공 +- 권한별 화면 노출 +- 상태 변경 및 알림 발송 + +따라서 서비스별로 별도 시스템을 만드는 대신 공통 플랫폼을 두고, 서비스별 차이는 `workspace`와 확장 테이블로 흡수하는 구조가 적절함. + +### 4.2 S/W Q&A 서비스 + +S/W Q&A 서비스는 각 프로그램 사용자 또는 외부 고객이 해당 앱의 전용 게시판에 접속하여 문의를 등록하고 답변을 받는 구조임. + +주요 기능은 다음과 같음. + +- 앱별 전용 게시판 제공 +- 문의 작성 및 담당자 답변 +- 비밀글 처리 +- 상태 변경 및 FAQ 추천 +- 필요 시 원격지원 일정 등록 +- 답변 등록 또는 상태 변경 시 알림 발송 + +### 4.3 사내 지원 서비스 + +사내 지원 서비스는 인트라넷에서 접근하는 업무 지원 포털 성격의 서비스임. + +지원 범위는 다음과 같음. + +- 물품신청 +- 도서 신청 +- 출장 차량 신청 +- 비품 대여 +- 사내 Q&A + +공통 처리 흐름은 다음과 같음. + +- 신청서 작성 +- 승인 또는 반려 +- 자산 배정 또는 차량 일정 등록 +- 처리 결과 알림 발송 + +### 4.4 운영 및 관리 페이지 + +사내 지원 서비스에는 담당자가 승인 또는 반려를 처리하는 운영 페이지가 필요함. 이는 단순 상태 변경 화면이 아니라 권한과 이력 관리의 중심 화면 역할을 담당함. + +운영 페이지의 필요 이유는 다음과 같음. + +- 승인 대기 건을 한 번에 조회 가능 +- 신청 상세 내용을 확인한 뒤 승인 또는 반려 처리 가능 +- 반려 사유 입력 및 승인 이력 관리 가능 +- 승인 이후 자산 배정, 차량 일정 등록, 후속 알림 발송까지 연결 가능 +- `can_approve`, `can_manage`, `page_scope` 권한과 직접 연결 가능 + +운영 페이지는 별도 시스템으로 분리하기보다 동일 플랫폼 내부의 권한 기반 메뉴로 구성하는 방식이 적절함. + +## 5. 핵심 구성요소 상세 + +### 5.1 사용자 접점 + +| 접점 | 설명 | +| --- | --- | +| 인트라넷 포털 | 사내 지원 서비스 진입점 | +| 각 S/W 프로그램 | S/W별 Q&A 서비스 진입점 | +| 운영 / 관리 페이지 | 담당자, 승인자, 관리자가 사용하는 내부 운영 화면 | + +인트라넷에서는 서비스 유형 기준으로 진입하고, S/W 프로그램에서는 앱 기준으로 진입함. 운영 담당자는 별도 운영 메뉴를 통해 진입하지만, 이 역시 내부적으로는 동일한 `workspace`와 권한 체계를 사용함. + +### 5.2 인증 및 권한 계층 + +BARON-SSO는 사용자 인증과 tenant 식별의 원본 시스템 역할을 담당함. 플랫폼은 BARON-SSO로부터 받은 `user_id`, `tenant_id`를 기준으로 내부 권한만 제어함. + +플랫폼 내부 권한 항목은 다음과 같음. + +- `can_read` +- `can_write` +- `can_manage` +- `can_approve` +- `page_scope` + +이 구조를 사용하면 사용자 기본 정보는 외부에서 일관되게 유지하고, 플랫폼 내부에서는 읽기, 쓰기, 승인, 관리 범위만 유연하게 제어 가능함. + +특히 승인 또는 반려 처리는 `can_approve` 권한이 있는 담당자만 수행하도록 제한하고, 운영 화면 접근 범위는 `page_scope`로 분리하는 방식이 적절함. + +### 5.3 핵심 서비스 계층 + +핵심 서비스 계층은 FastAPI 기반 API 서버로 구성하며, 다음 기능을 공통 처리함. + +| 기능 영역 | 설명 | +| --- | --- | +| Workspace Engine | 앱 또는 인트라넷 서비스를 내부 작업 단위로 매핑 | +| Ticket Engine | Q&A와 신청서를 공통 구조로 저장 및 처리 | +| Comment Engine | 답변, 처리 메모, 협업 이력 관리 | +| Attachment Engine | 첨부파일 저장 및 조회 관리 | +| FAQ Engine | 워크스페이스별 FAQ와 공지성 정보 제공 | +| Approval Extension | 신청 승인 및 반려 처리 | +| Asset Extension | 물품, 비품, 도서, 차량 자산 관리 | +| Remote Support Extension | S/W 문의의 원격지원 일정 및 처리 관리 | + +이 중 Approval Extension은 일반 사용자 화면보다 운영 페이지와 더 강하게 연결됨. 승인 담당자는 운영 페이지에서 대기 건 조회, 승인 또는 반려 처리, 반려 사유 작성, 후속 조치 등록을 수행함. + +### 5.4 데이터 및 외부 연동 계층 + +PostgreSQL은 플랫폼의 공통 데이터 저장소 역할을 담당함. 외부 연동은 알림과 운영 정보 보강 목적에 집중함. + +| 연동 대상 | 목적 | +| --- | --- | +| Naver Works API | 기본 알림 채널 | +| SMS Gateway | 네이버웍스 실패 또는 미보유 사용자 대체 알림 | +| 자산/조직도/기타 사내 API | 자산 정보 조회, 조직 기반 처리, 추가 업무 연계 | + +알림은 네이버웍스를 우선 사용하고, 발송 실패 또는 계정 미보유 시 SMS로 대체하는 정책을 적용함. + +## 6. DB 설계 방향 + +### 6.1 설계 원칙 + +- 사용자 마스터는 BARON-SSO에서 관리함. +- 로컬 DB는 권한, 워크스페이스, 업무 데이터, 알림 보조 정보만 관리함. +- Q&A와 사내 지원 요청은 분리 저장하지 않고 공통 티켓 구조로 통합함. +- 서비스별 차이는 확장 테이블로 분리하여 향후 신규 앱과 신규 사내 서비스가 추가되어도 구조 변경을 최소화함. +- 운영 페이지는 별도 사용자 테이블 없이 기존 권한 테이블과 승인 이력 테이블을 활용하여 구성함. + +### 6.2 데이터 영역 구분 + +| 데이터 영역 | 주요 테이블 | 설명 | +| --- | --- | --- | +| 서비스 마스터 | `software_apps`, `service_types`, `workspaces` | 진입 경로를 내부 서비스 단위로 매핑 | +| 권한 관리 | `user_workspace_access` | 사용자별 읽기, 쓰기, 관리, 승인 범위 제어 | +| 알림 보조 정보 | `user_notification_profiles` | 네이버웍스 키, 전화번호, SMS 수신 여부 관리 | +| 공통 업무 데이터 | `support_tickets`, `ticket_comments`, `attachments`, `faqs` | Q&A와 신청 데이터를 공통 구조로 관리 | +| 코드 관리 | `support_status_codes`, `support_category_codes`, `remote_support_status_codes` | 상태 및 분류 표준화 | +| 사내 지원 확장 | `request_approvals`, `assets`, `asset_allocations`, `vehicle_schedules` | 승인, 자산, 차량 업무 처리 | +| S/W 지원 확장 | `remote_support` | 원격지원 일정 및 처리 이력 관리 | +| 운영 이력 | `notification_logs` | 알림 발송 및 실패 이력 추적 | + +### 6.3 핵심 엔터티 설명 + +#### 6.3.1 workspace + +`workspace`는 이 설계의 중심 엔터티임. 외부에서는 S/W 앱 또는 인트라넷 서비스로 보이지만, 내부에서는 모두 `workspace`로 수렴함. 이 구조를 사용하면 신규 앱 또는 신규 사내 서비스가 생겨도 공통 기능을 재사용 가능함. + +#### 6.3.2 support_tickets + +Q&A 게시글과 각종 신청서를 별도 본문 테이블로 나누지 않고 `support_tickets`로 통합 관리함. 대신 `ticket_type`, `workspace_id`, `category_code`, `status_code`로 의미를 구분함. + +#### 6.3.3 user_workspace_access + +플랫폼은 사용자 상세 프로필을 저장하지 않지만, 어떤 사용자가 어떤 워크스페이스에서 무엇을 할 수 있는지는 반드시 관리해야 함. 이 역할을 `user_workspace_access`가 담당함. + +운영 페이지 관점에서는 이 테이블이 특히 중요함. 승인 담당자 여부, 운영 메뉴 접근 가능 여부, 특정 워크스페이스에 대한 승인 가능 범위가 모두 이 테이블의 권한 컬럼으로 제어되기 때문임. + +#### 6.3.4 request_approvals + +`request_approvals`는 승인 또는 반려가 실제로 수행된 결과를 남기는 이력 테이블임. 승인 담당자 정보, 처리 결과, 반려 사유, 승인 시각을 저장하므로 운영 페이지의 감사 추적과 처리 내역 조회에 직접 사용됨. + +## 7. DB 스키마 초안 + +```sql +-- 1. 소프트웨어 정보 +CREATE TABLE software_apps ( + id SERIAL PRIMARY KEY, + app_code VARCHAR(50) UNIQUE NOT NULL, + app_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 사내 서비스 유형 정보 +CREATE TABLE service_types ( + id SERIAL PRIMARY KEY, + service_code VARCHAR(50) UNIQUE NOT NULL, + service_name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. 공통 워크스페이스 마스터 +CREATE TABLE workspaces ( + id SERIAL PRIMARY KEY, + workspace_type VARCHAR(20) NOT NULL, + software_app_id INTEGER REFERENCES software_apps(id), + service_type_id INTEGER REFERENCES service_types(id), + workspace_code VARCHAR(50) UNIQUE NOT NULL, + workspace_name VARCHAR(100) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CHECK ( + (workspace_type = 'SOFTWARE_APP' AND software_app_id IS NOT NULL AND service_type_id IS NULL) + OR + (workspace_type = 'INTRANET_SERVICE' AND service_type_id IS NOT NULL AND software_app_id IS NULL) + ) +); + +-- 4. 사용자별 워크스페이스 접근 권한 및 역할 +CREATE TABLE user_workspace_access ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + workspace_role VARCHAR(20) NOT NULL DEFAULT 'USER', + can_read BOOLEAN DEFAULT TRUE, + can_write BOOLEAN DEFAULT FALSE, + can_manage BOOLEAN DEFAULT FALSE, + can_approve BOOLEAN DEFAULT FALSE, + page_scope VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, tenant_id, workspace_id) +); + +-- 5. 사용자 알림 보조 정보 +CREATE TABLE user_notification_profiles ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + user_type VARCHAR(20) NOT NULL DEFAULT 'INTERNAL', + phone_number VARCHAR(30), + naverworks_user_key VARCHAR(100), + sms_opt_in BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, tenant_id) +); + +-- 6. 공통 상태 코드 +CREATE TABLE support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 7. 공통 카테고리 코드 +CREATE TABLE support_category_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 8. 공통 게시글/신청 본문 +CREATE TABLE support_tickets ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + requester_id VARCHAR(100) NOT NULL, + requester_tenant_id VARCHAR(100) NOT NULL, + ticket_type VARCHAR(20) NOT NULL, + title VARCHAR(255) NOT NULL, + content TEXT, + category_code VARCHAR(20) REFERENCES support_category_codes(code), + status_code VARCHAR(20) NOT NULL DEFAULT 'OPEN' REFERENCES support_status_codes(code), + is_secret BOOLEAN DEFAULT FALSE, + requested_start_at TIMESTAMP, + requested_end_at TIMESTAMP, + priority VARCHAR(20) DEFAULT 'NORMAL', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 9. 댓글 및 처리 메모 +CREATE TABLE ticket_comments ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + author_id VARCHAR(100) NOT NULL, + author_tenant_id VARCHAR(100) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 10. 첨부파일 통합 관리 +CREATE TABLE attachments ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(20) NOT NULL, + parent_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + file_name VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 11. 사내 지원용 승인 이력 +CREATE TABLE request_approvals ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + approver_id VARCHAR(100) NOT NULL, + approver_tenant_id VARCHAR(100) NOT NULL, + approval_status VARCHAR(20) NOT NULL, + comment TEXT, + approved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 12. 자산/비품 마스터 +CREATE TABLE assets ( + id SERIAL PRIMARY KEY, + asset_code VARCHAR(50) UNIQUE NOT NULL, + asset_name VARCHAR(100) NOT NULL, + asset_type VARCHAR(30) NOT NULL, + quantity INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT TRUE, + location VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 13. 자산 배정 및 대여 이력 +CREATE TABLE asset_allocations ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + assignee_id VARCHAR(100), + assignee_tenant_id VARCHAR(100), + allocation_status VARCHAR(20) NOT NULL, + loaned_at TIMESTAMP, + due_at TIMESTAMP, + returned_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 14. 차량 운행 일정 +CREATE TABLE vehicle_schedules ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + asset_id INTEGER NOT NULL REFERENCES assets(id), + departure_at TIMESTAMP NOT NULL, + arrival_at TIMESTAMP, + destination VARCHAR(255), + driver_name VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 15. S/W Q&A용 원격 지원 상태 코드 +CREATE TABLE remote_support_status_codes ( + code VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + sort_order INTEGER NOT NULL +); + +-- 16. S/W Q&A용 원격 지원 로그 +CREATE TABLE remote_support ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER NOT NULL REFERENCES support_tickets(id), + status_code VARCHAR(20) REFERENCES remote_support_status_codes(code), + support_engineer_id VARCHAR(100), + support_engineer_tenant_id VARCHAR(100), + scheduled_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 17. 공통 FAQ +CREATE TABLE faqs ( + id SERIAL PRIMARY KEY, + workspace_id INTEGER NOT NULL REFERENCES workspaces(id), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 18. 알림 발송 이력 +CREATE TABLE notification_logs ( + id SERIAL PRIMARY KEY, + ticket_id INTEGER REFERENCES support_tickets(id), + recipient_id VARCHAR(100) NOT NULL, + recipient_tenant_id VARCHAR(100) NOT NULL, + channel VARCHAR(20) NOT NULL, + target_address VARCHAR(100), + delivery_status VARCHAR(20) NOT NULL, + fallback_channel VARCHAR(20), + error_message TEXT, + sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## 8. 정리 + +이 설계의 핵심은 S/W별 Q&A와 인트라넷 사내 지원을 서로 다른 시스템으로 분리하지 않고, BARON-SSO와 `workspace` 중심 공통 플랫폼으로 통합하는 데 있음. 사용자 정보는 BARON-SSO를 원본으로 유지하고, 플랫폼은 권한과 업무 처리에 집중함. 그 결과 서비스 확장성과 운영 일관성을 동시에 확보 가능함. + +다음 단계에서는 상태 코드 표준값, `page_scope` 체계, 주요 API 목록, 화면 구성도를 추가하면 구현 준비 수준의 설계 문서로 확장 가능함. diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_components_v2.md b/docs/관리페이지 md 파일/architecture_secretary_sso_components_v2.md new file mode 100644 index 0000000..9c5822a --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_components_v2.md @@ -0,0 +1,698 @@ +# 사내 지원 플랫폼 통합 아키텍처 설계서 + +## 1. 문서 목적 + +본 문서는 BARON-SSO 기반 공용 플랫폼으로 다음 두 서비스를 하나의 구조로 통합하는 방안을 설명함. + +- 각 S/W 프로그램에서 진입하는 S/W별 Q&A 플랫폼 +- 회사 인트라넷에서 진입하는 사내 지원 플랫폼 + +이번 설계의 핵심은 기존 ABC User Feedback 솔루션을 그대로 폐기하지 않고, 원본 게시글 저장소와 채널 관리 도구로 활용하면서 우리 시스템이 권한 제어, 승인 워크플로우, 동적 폼, 운영 대시보드를 담당하는 구조로 고도화하는 데 있음. + +## 2. 한눈에 보는 통합 방향 + +### 2.1 통합 대상 서비스 + +| 구분 | 설명 | +| --- | --- | +| S/W Q&A | 각 S/W 프로그램에서 로그인 후 해당 앱 전용 Q&A 채널로 연결되는 지원 서비스 | +| 사내 지원 | 인트라넷에서 진입하여 물품신청, 도서 신청, 출장 차량 신청, 비품 대여, 사내 Q&A를 처리하는 지원 서비스 | + +### 2.2 핵심 설계 판단 + +- 사용자 인증과 `tenant_id` 식별의 원본은 BARON-SSO가 담당함. +- ABC User Feedback는 게시글 원본 저장소이자 관리자 기반 채널/필드 관리 도구로 사용함. +- 우리 시스템은 FastAPI와 자체 DB를 통해 권한 분기, 상태 제어, 승인 이력, 운영 화면을 담당함. +- 외부 진입 경로의 `app_id`, `service_type_id`는 내부 `workspace`로 매핑함. +- 일반 사용자는 Next.js 동적 폼과 조회 화면을 사용하고, 담당자는 운영 페이지에서 승인/반려/후속 조치를 처리함. +- Q&A와 신청은 공통 티켓 모델로 다루되, 실제 원본 본문은 ABC에 저장하고 우리 DB에는 제어용 메타데이터와 매핑 정보를 저장함. + +### 2.3 역할 분리 요약 + +| 구성요소 | 주 역할 | 저장 데이터 | +| --- | --- | --- | +| BARON-SSO | 사용자 인증, `user_id`, `tenant_id` 발급 | 사용자/조직 원본 정보 | +| ABC User Feedback | 게시글 저장, 댓글/첨부 관리, 채널/필드 구성, 기본 목록/상세 UI | 피드백 원본 데이터, 채널 설정, 필드 값 | +| 우리 시스템 | 권한 필터링, 승인 워크플로우, 상태 제어, 운영 페이지, 동적 폼, 외부 연동 오케스트레이션 | 권한, 워크스페이스, 매핑, 승인, 자산, 차량, 알림, 운영 이력 | + +## 3. High-Level Architecture + +### 3.1 아키텍처 관점 + +| 관점 | 설명 | +| --- | --- | +| 사용자 접점 | 사용자가 어디서 진입하는지 | +| 인증 계층 | BARON-SSO가 어디에서 인증을 담당하는지 | +| 데이터 저장 계층 | ABC와 자체 DB가 어떤 데이터를 나눠 저장하는지 | +| 제어 계층 | 권한, 승인, 운영 로직이 어디에서 처리되는지 | +| 외부 연동 | 알림, 자산, 조직도, 기타 사내 API가 어디에서 연결되는지 | + +### 3.2 High-Level Architecture 다이어그램 + +아래 다이어그램은 처리 행위를 화살표 라벨로 표시하고, 결과가 쌓이거나 보여지는 지점은 사각형 박스로 구분한 구조임. + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f7f7f7,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + subgraph U[사용자 영역] + U1[인트라넷 사용자] + U2[S/W 사용자] + U3[운영 담당자] + end + + subgraph E[진입 채널] + E1[인트라넷 포털] + E2[각 S/W 프로그램] + E3[운영 메뉴] + end + + SSO[BARON-SSO\nOAuth 2.0 / OIDC]:::external + + subgraph UI[화면 계층] + N1[Next.js 사용자 화면\n동적 폼 / 조회 / 상세]:::result + N2[Next.js 운영 페이지\n승인 / 반려 / 후속 조치]:::result + end + + subgraph CTL[우리 시스템\nFastAPI + 자체 DB] + F1[권한 분기 엔진\nuser_id + tenant_id + workspace]:::control + F2[업무 프로세스 엔진\n상태 제어 / 승인 워크플로우 / 후속 조치]:::control + F3[브릿지 API\nABC API 연동 / 매핑 기록]:::control + D1[자체 제어 DB\n권한 / 매핑 / 승인 / 자산 / 차량 / 알림]:::result + end + + subgraph ABC[ABC User Feedback] + A1[채널 / 필드 관리자 UI]:::result + A2[Feedback API\nPOST /feedbacks 등]:::control + A3[원본 데이터 저장소\n제목 / 본문 / 댓글 / 첨부 / 필드값]:::result + A4[기본 목록 / 상세 UI]:::result + end + + subgraph X[외부 연동] + X1[Naver Works API]:::external + X2[SMS Gateway]:::external + X3[자산 / 조직도 / 기타 사내 API]:::external + end + + U1 -->|인트라넷 서비스 진입| E1 + U2 -->|앱 내부 지원 메뉴 진입| E2 + U3 -->|운영 메뉴 진입| E3 + + E1 -->|사용자 화면 호출| N1 + E2 -->|사용자 화면 호출| N1 + E3 -->|운영 페이지 호출| N2 + + N1 -->|SSO 로그인 요청| SSO + N2 -->|운영자 인증 요청| SSO + SSO -->|user_id, tenant_id, 토큰 반환| N1 + SSO -->|운영 권한 토큰 반환| N2 + + N1 -->|폼/목록 조회 요청| F1 + N1 -->|신청/문의 등록 요청| F3 + N2 -->|승인 대기/처리 요청| F2 + + F1 -->|workspace 매핑 및 권한 판정| D1 + F2 -->|승인 이력 / 상태 저장| D1 + F3 -->|매핑 정보 조회 및 기록| D1 + + F3 -->|채널/필드 조회| A1 + F3 -->|원본 게시글 생성/조회| A2 + A2 -->|피드백 원본 저장| A3 + A2 -->|기본 목록/상세 제공| A4 + + F2 -->|알림 발송 요청| X1 + F2 -->|실패 시 대체 발송| X2 + F2 -->|자산/차량 후속 처리| X3 +``` + +### 3.3 전체 처리 흐름 요약 + +1. 사용자는 인트라넷 포털 또는 S/W 프로그램에서 Next.js 사용자 화면으로 진입함. +2. 사용자 화면은 BARON-SSO를 통해 인증하고 `user_id`, `tenant_id`를 확보함. +3. 우리 시스템의 권한 분기 엔진은 진입 경로를 `workspace`로 매핑하고 조회/등록 가능 범위를 판단함. +4. 사용자가 문의 또는 신청서를 등록하면 브릿지 API가 ABC Feedback API로 원본 데이터를 저장함. +5. 동시에 우리 DB에는 `workspace`, 상태, 요청 유형, ABC 피드백 ID, 기존 시스템 ID, 승인 필요 여부 등 제어 정보를 저장함. +6. 담당자는 운영 페이지에서 승인 대기 건을 조회하고 `request_approvals` 기반으로 승인/반려/후속 조치를 처리함. +7. 승인 결과와 상태 변경은 우리 DB에 기록되고, 필요 시 Naver Works 또는 SMS, 자산/차량 API 연동이 수행됨. + +## 4. 서비스 책임 분리 + +### 4.1 ABC User Feedback의 역할 + +ABC User Feedback는 기능 저장소이자 데이터 저장소로 사용함. 즉, 사용자에게 보이는 게시글 원본과 채널 구조는 ABC가 책임지고, 우리 시스템은 이를 직접 대체하지 않음. + +ABC가 담당하는 범위는 다음과 같음. + +- 게시글 원본 데이터 저장 +- 제목, 본문, 댓글, 첨부파일 저장 +- 채널 생성 및 채널별 필드 구성 +- 신청서 항목을 표현하는 커스텀 필드 저장 +- 시스템 API를 통한 피드백 생성 및 조회 +- 기본 목록/상세 UI 제공 + +즉, ABC는 "무엇이 기록되었는가"를 보존하는 원본 시스템임. + +### 4.2 우리 시스템의 역할 + +우리 시스템은 FastAPI, Next.js, 자체 DB를 사용하여 ABC 위에 제어 계층을 추가함. + +우리 시스템이 담당하는 범위는 다음과 같음. + +- `tenant_id`와 `workspace` 기준 권한 분기 +- 사용자별 조회 가능 데이터 필터링 +- 신청서 상태 제어: 대기, 승인, 반려, 처리 완료 +- `request_approvals` 기반 승인 워크플로우 처리 +- 자산 배정, 차량 배차, 원격지원 등 확장 프로세스 처리 +- 운영 대시보드 제공 +- Next.js 동적 폼 렌더링 및 유효성 검사 +- 기존 데이터 마이그레이션 시 ABC ID와 기존 글 ID 매핑 관리 + +즉, 우리 시스템은 "누가 무엇을 볼 수 있고, 어떤 절차로 처리되는가"를 책임지는 지능형 제어 엔진임. + +### 4.3 화면 구성 원칙 + +| 화면 | 주 시스템 | 설명 | +| --- | --- | --- | +| 일반 사용자 입력 화면 | 우리 시스템 | 진입 경로별 동적 폼, 유효성 검사, 등록 프로세스 제어 | +| 일반 사용자 목록/상세 | 혼합 | 우리 시스템의 권한 필터링 결과와 ABC 원본 데이터를 조합하여 노출 | +| 운영 페이지 | 우리 시스템 | 승인/반려/후속 조치 전용 대시보드 | +| 채널/필드 관리자 화면 | ABC User Feedback | 채널 생성, 필드 구성, 스키마 관리 | + +## 5. DB 설계 방향 + +### 5.1 설계 원칙 + +- 사용자 마스터와 조직 정보는 BARON-SSO에서 관리함. +- 게시글 원본, 댓글, 첨부, 채널 필드 값은 ABC에 저장함. +- 우리 DB는 제어용 메타데이터와 운영용 확장 데이터만 저장함. +- `support_tickets`는 내부 티켓 식별자이자 ABC 피드백과 연결되는 제어 엔트리 역할을 수행함. +- 마이그레이션과 운영 연계에 필요한 식별자 매핑은 별도 매핑 테이블로 관리함. +- 승인, 자산, 차량, 원격지원은 ABC 원본과 느슨하게 연결된 확장 테이블로 관리함. + +### 5.2 데이터 저장 책임 분리 + +| 데이터 범주 | 저장 위치 | 설명 | +| --- | --- | --- | +| 사용자 인증 정보 | BARON-SSO | `user_id`, `tenant_id`, 토큰, 조직 기준 원본 | +| 게시글 원본 | ABC User Feedback | 제목, 본문, 댓글, 첨부, 사용자 입력 필드 값 | +| 권한/워크스페이스 | 우리 DB | `workspace`, 역할, 읽기/쓰기/승인/관리 권한 | +| 제어용 티켓 메타데이터 | 우리 DB | 내부 티켓 ID, 상태, ABC 피드백 ID, 승인 필요 여부 | +| 업무 확장 데이터 | 우리 DB | 승인, 자산, 차량, 원격지원, 알림 로그 | +| 마이그레이션 매핑 | 우리 DB | 기존 시스템 ID와 ABC 피드백 ID, 내부 티켓 ID 연결 | + +### 5.3 핵심 관계 구조 + +```mermaid +flowchart TD + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.2px,color:#0f172a; + classDef control fill:#f7f7f7,stroke:#334155,stroke-width:1.2px,color:#111827; + + W[workspaces]:::result -->|1:N 서비스 범위 정의| UWA[user_workspace_access]:::result + W -->|1:N 내부 티켓 소속| ST[support_tickets]:::result + ST -->|1:1 또는 1:N ABC 원본 연결| AFM[abc_feedback_mappings]:::result + AFM -->|ABC feedback_id 참조 기록| ABCREF[ABC feedback]:::control + ST -->|1:N 승인 이력| RA[request_approvals]:::result + ST -->|1:N 자산 배정| AA[asset_allocations]:::result + ST -->|1:N 차량 일정| VS[vehicle_schedules]:::result + ST -->|1:N 원격지원 이력| RS[remote_support]:::result + ST -->|1:N 알림 이력| NL[notification_logs]:::result + ST -->|1:N 마이그레이션 추적| MM[migration_mappings]:::result +``` + +## 6. DB 스키마 상세화 + +### 6.1 자체 DB 데이터 영역 + +| 데이터 영역 | 주요 테이블 | 설명 | +| --- | --- | --- | +| 서비스 마스터 | `software_apps`, `service_types`, `workspaces` | 진입 채널을 내부 `workspace`로 매핑 | +| 권한 관리 | `support_users`, `support_roles`, `support_role_assignments`, `user_workspace_access` | 내부 사용자·역할 할당과 workspace 유효 권한 제어 | +| 폼/채널 연동 | `workspace_channel_mappings`, `workspace_field_mappings` | `workspace`와 ABC 채널/필드 연결 | +| 공통 티켓 메타데이터 | `support_tickets` | 내부 상태, 요청자, 티켓 유형, 우선순위 관리 | +| ABC 연동 매핑 | `abc_feedback_mappings` | 내부 티켓과 ABC `feedback_id` 연결 | +| 마이그레이션 추적 | `migration_mappings`, `migration_batches` | 기존 글 ID, 기존 첨부 ID, ABC ID 적재 이력 관리 | +| 승인/운영 | `request_approvals`, `notification_logs` | 승인 및 알림 처리 이력 | +| 업무 확장 | `assets`, `asset_allocations`, `vehicle_schedules`, `remote_support` | 자산, 차량, 원격지원 업무 관리 | + +### 6.2 핵심 상세 테이블 제안 + +아래 스키마는 ABC와 우리 DB의 역할 분리를 반영한 초안임. + +```sql +CREATE TABLE software_apps ( + id BIGINT NOT NULL AUTO_INCREMENT, + app_code VARCHAR(50) NOT NULL, + app_name VARCHAR(100) NOT NULL, + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_software_apps_app_code (app_code), + UNIQUE KEY uq_software_apps_app_name (app_name) +); + +CREATE TABLE service_types ( + id BIGINT NOT NULL AUTO_INCREMENT, + service_code VARCHAR(50) NOT NULL, + service_name VARCHAR(100) NOT NULL, + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_service_types_service_code (service_code), + UNIQUE KEY uq_service_types_service_name (service_name) +); + +CREATE TABLE workspaces ( + id BIGINT NOT NULL AUTO_INCREMENT, + workspace_type VARCHAR(20) NOT NULL, + software_app_id BIGINT NULL, + service_type_id BIGINT NULL, + workspace_code VARCHAR(50) NOT NULL, + workspace_name VARCHAR(100) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_workspaces_workspace_code (workspace_code), + CONSTRAINT fk_workspaces_software_app + FOREIGN KEY (software_app_id) REFERENCES software_apps(id), + CONSTRAINT fk_workspaces_service_type + FOREIGN KEY (service_type_id) REFERENCES service_types(id), + CONSTRAINT chk_workspaces_scope + CHECK ( + (workspace_type = 'SOFTWARE_APP' AND software_app_id IS NOT NULL AND service_type_id IS NULL) + OR + (workspace_type = 'INTRANET_SERVICE' AND service_type_id IS NOT NULL AND software_app_id IS NULL) + ) +); + +CREATE TABLE user_workspace_access ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id VARCHAR(100) NOT NULL, + tenant_id VARCHAR(100) NOT NULL, + workspace_id BIGINT NOT NULL, + workspace_role VARCHAR(30) NOT NULL DEFAULT 'USER', + can_read BOOLEAN NOT NULL DEFAULT TRUE, + can_write BOOLEAN NOT NULL DEFAULT FALSE, + can_manage BOOLEAN NOT NULL DEFAULT FALSE, + can_approve BOOLEAN NOT NULL DEFAULT FALSE, + page_scope VARCHAR(50), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_user_workspace_access_user_tenant_workspace (user_id, tenant_id, workspace_id), + CONSTRAINT fk_user_workspace_access_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id) +); + +CREATE TABLE workspace_channel_mappings ( + id BIGINT NOT NULL AUTO_INCREMENT, + workspace_id BIGINT NOT NULL, + abc_channel_id VARCHAR(100) NOT NULL, + abc_channel_key VARCHAR(100), + form_template_version VARCHAR(30), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_workspace_channel_mappings_workspace_channel (workspace_id, abc_channel_id), + CONSTRAINT fk_workspace_channel_mappings_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id) +); + +CREATE TABLE workspace_field_mappings ( + id BIGINT NOT NULL AUTO_INCREMENT, + workspace_id BIGINT NOT NULL, + abc_channel_id VARCHAR(100) NOT NULL, + abc_field_key VARCHAR(100) NOT NULL, + local_field_code VARCHAR(100) NOT NULL, + field_label VARCHAR(100) NOT NULL, + field_type VARCHAR(30) NOT NULL, + is_required BOOLEAN NOT NULL DEFAULT FALSE, + sort_order INT NOT NULL DEFAULT 0, + validation_rule JSON, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_workspace_field_mappings_workspace_channel_field (workspace_id, abc_channel_id, abc_field_key), + CONSTRAINT fk_workspace_field_mappings_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id) +); + +CREATE TABLE support_status_codes ( + code VARCHAR(20) NOT NULL, + name VARCHAR(50) NOT NULL, + sort_order INT NOT NULL, + PRIMARY KEY (code) +); + +CREATE TABLE support_category_codes ( + code VARCHAR(20) NOT NULL, + name VARCHAR(50) NOT NULL, + sort_order INT NOT NULL, + PRIMARY KEY (code) +); + +CREATE TABLE support_tickets ( + id BIGINT NOT NULL AUTO_INCREMENT, + workspace_id BIGINT NOT NULL, + requester_id VARCHAR(100) NOT NULL, + requester_tenant_id VARCHAR(100) NOT NULL, + ticket_type VARCHAR(30) NOT NULL, + source_system VARCHAR(30) NOT NULL DEFAULT 'ABC', + title VARCHAR(255) NOT NULL, + category_code VARCHAR(20), + status_code VARCHAR(20) NOT NULL, + is_secret BOOLEAN NOT NULL DEFAULT FALSE, + requires_approval BOOLEAN NOT NULL DEFAULT FALSE, + current_assignee_id VARCHAR(100), + current_assignee_tenant_id VARCHAR(100), + requested_start_at TIMESTAMP NULL, + requested_end_at TIMESTAMP NULL, + priority VARCHAR(20) NOT NULL DEFAULT 'NORMAL', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_support_tickets_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id), + CONSTRAINT fk_support_tickets_category_code + FOREIGN KEY (category_code) REFERENCES support_category_codes(code), + CONSTRAINT fk_support_tickets_status_code + FOREIGN KEY (status_code) REFERENCES support_status_codes(code) +); + +CREATE TABLE abc_feedback_mappings ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NOT NULL, + workspace_id BIGINT NOT NULL, + abc_channel_id VARCHAR(100) NOT NULL, + abc_feedback_id VARCHAR(100) NOT NULL, + abc_feedback_url TEXT, + sync_status VARCHAR(20) NOT NULL DEFAULT 'SYNCED', + last_synced_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_abc_feedback_mappings_ticket_id (ticket_id), + UNIQUE KEY uq_abc_feedback_mappings_feedback_id (abc_feedback_id), + CONSTRAINT fk_abc_feedback_mappings_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id), + CONSTRAINT fk_abc_feedback_mappings_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id) +); + +CREATE TABLE migration_batches ( + id BIGINT NOT NULL AUTO_INCREMENT, + batch_name VARCHAR(100) NOT NULL, + source_system VARCHAR(50) NOT NULL, + started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + status VARCHAR(20) NOT NULL DEFAULT 'RUNNING', + executed_by VARCHAR(100), + notes TEXT, + PRIMARY KEY (id) +); + +CREATE TABLE migration_mappings ( + id BIGINT NOT NULL AUTO_INCREMENT, + batch_id BIGINT NOT NULL, + source_system VARCHAR(50) NOT NULL, + source_entity_type VARCHAR(30) NOT NULL, + source_entity_id VARCHAR(100) NOT NULL, + source_parent_id VARCHAR(100), + workspace_id BIGINT NOT NULL, + ticket_id BIGINT NULL, + abc_feedback_id VARCHAR(100), + migration_status VARCHAR(20) NOT NULL, + error_message TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_migration_mappings_source_entity (source_system, source_entity_type, source_entity_id), + CONSTRAINT fk_migration_mappings_batch + FOREIGN KEY (batch_id) REFERENCES migration_batches(id), + CONSTRAINT fk_migration_mappings_workspace + FOREIGN KEY (workspace_id) REFERENCES workspaces(id), + CONSTRAINT fk_migration_mappings_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id) +); + +CREATE TABLE request_approvals ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NOT NULL, + approver_id VARCHAR(100) NOT NULL, + approver_tenant_id VARCHAR(100) NOT NULL, + approval_status VARCHAR(20) NOT NULL, + comment TEXT, + approved_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_request_approvals_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id) +); + +CREATE TABLE assets ( + id BIGINT NOT NULL AUTO_INCREMENT, + asset_code VARCHAR(50) NOT NULL, + asset_name VARCHAR(100) NOT NULL, + asset_type VARCHAR(30) NOT NULL, + quantity INT NOT NULL DEFAULT 1, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + location VARCHAR(100), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_assets_asset_code (asset_code) +); + +CREATE TABLE asset_allocations ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NOT NULL, + asset_id BIGINT NOT NULL, + assignee_id VARCHAR(100), + assignee_tenant_id VARCHAR(100), + allocation_status VARCHAR(20) NOT NULL, + loaned_at TIMESTAMP NULL, + due_at TIMESTAMP NULL, + returned_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_asset_allocations_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id), + CONSTRAINT fk_asset_allocations_asset + FOREIGN KEY (asset_id) REFERENCES assets(id) +); + +CREATE TABLE vehicle_schedules ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NOT NULL, + asset_id BIGINT NOT NULL, + departure_at TIMESTAMP NOT NULL, + arrival_at TIMESTAMP NULL, + destination VARCHAR(255), + driver_name VARCHAR(100), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_vehicle_schedules_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id), + CONSTRAINT fk_vehicle_schedules_asset + FOREIGN KEY (asset_id) REFERENCES assets(id) +); + +CREATE TABLE remote_support ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NOT NULL, + status_code VARCHAR(20) NOT NULL, + support_engineer_id VARCHAR(100), + support_engineer_tenant_id VARCHAR(100), + scheduled_time TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_remote_support_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id) +); + +CREATE TABLE notification_logs ( + id BIGINT NOT NULL AUTO_INCREMENT, + ticket_id BIGINT NULL, + recipient_id VARCHAR(100) NOT NULL, + recipient_tenant_id VARCHAR(100) NOT NULL, + channel VARCHAR(20) NOT NULL, + target_address VARCHAR(100), + delivery_status VARCHAR(20) NOT NULL, + fallback_channel VARCHAR(20), + error_message TEXT, + sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_notification_logs_ticket + FOREIGN KEY (ticket_id) REFERENCES support_tickets(id) +); +``` + +### 6.3 테이블 간 연동 해석 + +- `workspaces`는 외부 진입 채널과 내부 처리 단위를 연결하는 기준 테이블임. +- `workspace_channel_mappings`는 하나의 `workspace`가 어떤 ABC 채널을 사용하는지 정의함. +- `workspace_field_mappings`는 Next.js 동적 폼과 ABC 커스텀 필드를 동일한 규칙으로 묶어줌. +- `support_tickets`는 우리 시스템이 상태와 권한을 제어하기 위한 내부 티켓 헤더임. +- `abc_feedback_mappings`는 내부 티켓과 ABC 원본 피드백을 1:1로 연결하는 핵심 브릿지 테이블임. +- `migration_mappings`는 기존 시스템 글, 댓글, 첨부가 어느 내부 티켓과 ABC 피드백으로 적재되었는지 추적함. + +## 7. API 엔드포인트 설계 + +### 7.1 API 설계 원칙 + +- 외부 클라이언트는 직접 ABC API를 호출하지 않고 우리 FastAPI를 경유함. +- FastAPI는 SSO 토큰을 검증하고 `tenant_id`, `workspace`, 권한 범위를 해석함. +- 등록 시에는 ABC API와 자체 DB 쓰기가 하나의 업무 트랜잭션처럼 동작해야 함. +- 조회 시에는 우리 DB에서 권한 필터링을 먼저 수행하고, 필요 시 ABC 원본 데이터를 조합하여 응답함. + +### 7.2 브릿지 API 목록 + +| 메서드 | 경로 | 목적 | +| --- | --- | --- | +| `POST` | `/api/workspaces/{workspaceCode}/tickets` | 사용자 문의/신청 등록, ABC 피드백 생성, 내부 티켓 및 매핑 저장 | +| `GET` | `/api/workspaces/{workspaceCode}/tickets` | 권한 필터링된 목록 조회 | +| `GET` | `/api/workspaces/{workspaceCode}/tickets/{ticketId}` | 내부 상태 + ABC 원본 상세 조합 조회 | +| `POST` | `/api/workspaces/{workspaceCode}/tickets/{ticketId}/comments` | 댓글 또는 처리 메모 등록 | +| `POST` | `/api/workspaces/{workspaceCode}/tickets/{ticketId}/attachments` | 첨부 업로드 브릿지 처리 | +| `GET` | `/api/workspaces/{workspaceCode}/form-template` | 동적 폼 렌더링용 필드 구성 조회 | +| `GET` | `/api/admin/workspaces/{workspaceCode}/pending-approvals` | 운영 페이지 승인 대기 목록 조회 | +| `POST` | `/api/admin/tickets/{ticketId}/approve` | 승인 처리 및 `request_approvals` 기록 | +| `POST` | `/api/admin/tickets/{ticketId}/reject` | 반려 처리 및 반려 사유 기록 | +| `POST` | `/api/admin/tickets/{ticketId}/follow-up/assets` | 자산 배정 후속 처리 | +| `POST` | `/api/admin/tickets/{ticketId}/follow-up/vehicle` | 차량 배차 후속 처리 | + +### 7.3 등록 브릿지 API 처리 순서 + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.2px,color:#0f172a; + classDef control fill:#f7f7f7,stroke:#334155,stroke-width:1.2px,color:#111827; + + C1[클라이언트 요청]:::result -->|SSO 토큰 포함| C2[FastAPI 브릿지]:::control + C2 -->|workspace 및 권한 확인| C3[내부 사용자·역할 및 user_workspace_access 조회]:::result + C2 -->|필드 매핑 로드| C4[workspace_field_mappings 조회]:::result + C2 -->|ABC payload 변환| C5[ABC POST /feedbacks 호출]:::control + C5 -->|feedback_id 반환| C6[ABC 원본 저장 완료]:::result + C2 -->|내부 티켓 생성| C7[support_tickets 저장]:::result + C2 -->|ABC 연결 기록| C8[abc_feedback_mappings 저장]:::result + C2 -->|승인 필요 여부 판정| C9[초기 상태 결정]:::result +``` + +### 7.4 조회 API 권한 필터링 규칙 + +| 시나리오 | 필터 기준 | +| --- | --- | +| 일반 사용자 내 글 조회 | `requester_id = current_user_id` and `requester_tenant_id = current_tenant_id` | +| 워크스페이스 담당자 조회 | `user_workspace_access.can_manage = true` | +| 승인 담당자 조회 | `user_workspace_access.can_approve = true` and `page_scope` 일치 | +| 외부 고객 조회 | 본인 글만 조회, 비밀글은 작성자/담당자만 허용 | + +### 7.5 예시 응답 모델 + +```json +{ + "ticketId": 1024, + "workspaceCode": "INTRA_BOOK_REQUEST", + "statusCode": "PENDING_APPROVAL", + "approvalRequired": true, + "abc": { + "channelId": "book-request-channel", + "feedbackId": "fb_938421", + "detailUrl": "https://abc.example.com/feedbacks/fb_938421" + }, + "requester": { + "userId": "u1001", + "tenantId": "baron" + } +} +``` + +## 8. 마이그레이션 매핑 로직 설계 + +### 8.1 마이그레이션 목표 + +기존 시스템의 게시글, 신청서, 댓글, 첨부를 ABC 원본 구조로 적재하면서, 동시에 우리 시스템의 내부 티켓과 매핑 정보를 남겨 이후 조회, 권한 제어, 운영 처리에 문제없이 연결되도록 해야 함. + +### 8.2 배치 처리 단계 + +1. 기존 시스템에서 대상 데이터와 첨부 메타데이터를 추출함. +2. 기존 글 유형을 `workspace`와 `ticket_type`으로 변환함. +3. 필드 매핑 규칙에 따라 ABC 커스텀 필드 payload를 생성함. +4. ABC API로 게시글 원본을 생성하고 `abc_feedback_id`를 수신함. +5. 우리 DB의 `support_tickets`에 내부 제어용 티켓을 생성함. +6. `abc_feedback_mappings`와 `migration_mappings`에 연결 정보를 저장함. +7. 댓글과 첨부는 원본 글 적재 완료 후 후속 단계로 연결함. +8. 실패 건은 `migration_status = 'FAILED'`와 `error_message`로 남기고 재처리 가능하게 함. + +### 8.3 마이그레이션 흐름 다이어그램 + +```mermaid +flowchart TD + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.2px,color:#0f172a; + classDef control fill:#f7f7f7,stroke:#334155,stroke-width:1.2px,color:#111827; + + M1[기존 시스템 데이터]:::result -->|배치가 원본 추출| M2[Migration Script]:::control + M2 -->|workspace 규칙 적용| M3[workspace 결정 결과]:::result + M2 -->|ABC payload 생성| M4[ABC POST /feedbacks]:::control + M4 -->|abc_feedback_id 수신| M5[ABC 원본 생성 결과]:::result + M2 -->|내부 티켓 생성| M6[support_tickets]:::result + M2 -->|브릿지 연결 저장| M7[abc_feedback_mappings]:::result + M2 -->|이관 추적 저장| M8[migration_mappings]:::result + M2 -->|실패 사유 기록| M9[재처리 대상 목록]:::result +``` + +### 8.4 마이그레이션 스크립트 의사코드 + +```python +def migrate_record(source_record): + workspace = resolve_workspace(source_record.app_id, source_record.service_type) + template = load_workspace_template(workspace.id) + abc_payload = build_abc_payload(source_record, template) + + abc_feedback = abc_client.create_feedback( + channel_id=template.abc_channel_id, + payload=abc_payload, + ) + + ticket = create_support_ticket( + workspace_id=workspace.id, + requester_id=source_record.user_id, + requester_tenant_id=source_record.tenant_id, + ticket_type=source_record.ticket_type, + title=source_record.title, + status_code=initial_status(source_record), + ) + + create_abc_feedback_mapping( + ticket_id=ticket.id, + workspace_id=workspace.id, + abc_channel_id=template.abc_channel_id, + abc_feedback_id=abc_feedback.id, + ) + + create_migration_mapping( + source_system="legacy_support", + source_entity_type="POST", + source_entity_id=source_record.legacy_id, + workspace_id=workspace.id, + ticket_id=ticket.id, + abc_feedback_id=abc_feedback.id, + migration_status="SYNCED", + ) +``` + +### 8.5 운영상 주의점 + +- 댓글과 첨부는 원본 글 생성 성공 이후 단계적으로 적재해야 함. +- 기존 시스템 ID 중복을 막기 위해 `migration_mappings`에 유니크 제약이 필요함. +- 재실행 가능성을 고려해 배치는 멱등적으로 설계해야 함. +- ABC 적재 성공 후 내부 DB 저장 실패가 발생하면 보상 처리 또는 재동기화 큐가 필요함. + +## 9. 정리 + +이 설계의 핵심은 S/W별 Q&A와 인트라넷 사내 지원을 하나의 `workspace` 기반 플랫폼으로 통합하되, 시스템 역할을 명확히 나누는 데 있음. + +- ABC User Feedback는 게시글 원본 저장소이자 채널/필드 관리 도구임. +- 우리 시스템은 FastAPI와 자체 DB를 통해 권한 분기, 승인 워크플로우, 운영 페이지, 동적 폼, 외부 연동을 담당함. +- BARON-SSO는 사용자와 `tenant_id`의 원본 인증 계층으로 유지됨. +- `abc_feedback_mappings`, `workspace_channel_mappings`, `migration_mappings`가 두 시스템을 연결하는 핵심 브릿지 역할을 수행함. + +다음 단계에서는 상태 코드 표준값, `page_scope` 체계, 실제 ABC API 스펙, 운영 페이지 화면 와이어프레임을 추가하면 구현 준비 수준의 설계 문서로 확장 가능함. diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_role_access.md b/docs/관리페이지 md 파일/architecture_secretary_sso_role_access.md new file mode 100644 index 0000000..4650ff7 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_role_access.md @@ -0,0 +1,228 @@ +# BARON-SSO 권한 및 접근 제어 설계 + +## 1. 문서 목적 + +본 문서는 BARON-SSO 연동 시점에 필요한 역할 분리, 테넌트 기반 1차 분기, 로그인 후 페이지 분기, 프로젝트 접근 제어 규칙을 별도로 정리한 문서임. + +핵심 목적은 다음과 같음. + +- 개발자, 프로젝트 관리자, 사용자의 권한 범위를 명확히 구분함. +- BARON-SSO `tenant_id` 와 우리 시스템의 페이지 분기 기준을 연결함. +- BARON-SSO는 인증과 현재 테넌트 정보 제공까지만 담당하고, 최종 권한은 내부 DB에서 관리한다는 원칙을 고정함. +- 프로젝트, 문의 구분, 관리자 콘솔 처리 구조를 현재 구현 방향 기준으로 문서화함. +- 이후 실제 SSO 구현, 라우팅, 내부 권한 테이블 설계의 기준 문서로 사용함. + +## 2. 적용 대상 범위 + +- 인증 원본: BARON-SSO +- 사용자 화면: `/support/[workspaceCode]/new`, `/support/[workspaceCode]/list`, `/support/[workspaceCode]/[ticketId]` +- 관리자 콘솔: `/main/project/[projectId]/feedback?channelId=[channelId]` +- 운영 보조 화면: `/ops`, `/admin/issues` +- 제어 백엔드: `apps/secretary-api` +- 관리자 API: `apps/api` + +## 3. 역할 정의 + +| 역할 | 영문 역할명 | 주요 권한 | 접근 범위 | 로그인 후 기본 진입 화면 | +| ------ | ------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------- | --------------------------------- | +| 개발자 | `SYSTEM_ADMIN`, `SUPER_ADMIN` | 시스템 전체 설정, SSO 연동 관리, 프로젝트 생성 및 삭제, 권한 정책 변경 | 모든 프로젝트, 모든 관리자 기능 | 관리자 콘솔 또는 시스템 설정 화면 | +| 관리자 | `PROJECT_MANAGER` | 배정된 프로젝트의 피드백 조회, 댓글 처리, 이슈 연동, 권한 설정, 운영 분류 처리 | 내부 DB에 배정된 프로젝트 | 관리자 콘솔 | +| 사용자 | `END_USER`, `FEEDBACK_PROVIDER` | 피드백 작성, 본인 글 조회, 본인 글 상태 확인, 비밀글 작성, 공개된 Q&A 확인 | 본인이 진입한 프로젝트와 본인 작성 데이터 | 사용자 피드백 작성 페이지 | + +## 4. 권한 모델 핵심 원칙 + +### 4.1 1차 판정 기준 + +- 로그인 자체는 모두 BARON-SSO에서 처리함. +- 우리 시스템은 BARON-SSO 세션 또는 userinfo 기준으로 최소 `sub` 또는 내부 매핑 가능한 식별자와 `tenant_id` 를 받음. +- `tenant_id` 는 현재 사용자가 어떤 테넌트 문맥으로 진입했는지 확인하는 1차 분기 힌트로만 사용함. + +### 4.2 2차 판정 기준 + +- 로그인 성공 직후 내부 DB에서 사용자 매핑과 역할을 조회함. +- 관리자 콘솔 진입 여부는 BARON-SSO 역할이 아니라 내부 DB의 프로젝트 관리자 권한으로 판단함. +- 그 외 사용자는 일반 사용자로 처리함. +- 추가 세부 권한은 우선 `user + project + role` 조합으로 제한하고, 채널 단위 권한은 추후 필요 시 확장함. + +### 4.3 내부 권한 DB 책임 분리 + +- BARON-SSO는 인증과 `sso_sub`, `tenant_id` 등 사용자 식별 문맥만 제공함. +- `baron_support`의 `support_users`, `support_roles`, `support_role_assignments`가 Secretary 사용자와 최종 권한의 원본임. +- `user_workspace_access`는 역할 할당에 따른 workspace별 유효 권한을 저장하는 실행용 접근표임. +- ABC User Feedback의 `users`, `roles`, `members`는 ABC 자체 피드백/관리자 콘솔 CRUD의 권한만 담당함. +- Secretary의 일반 사용자/운영 권한을 ABC의 `users.type`, `roles`, `members`에서 조회하거나 추론하지 않음. + +### 4.4 3차 판정 기준 + +- 관리자 콘솔에 진입한 이후에는 프로젝트별 접근 권한을 다시 확인함. +- 초기에는 전체 프로젝트 관리자만 두고, 채널 관리자는 운영 요구가 생길 때 별도 역할로 확장함. +- 관리자 권한 설정은 관리자 콘솔의 별도 설정 페이지에서 처리함. + +## 5. 테넌트 전략 + +### 5.1 테넌트 사용 원칙 + +- BARON-SSO의 현재 테넌트 정보는 로그인 문맥과 초기 이동 경로를 정하는 보조 정보로 사용함. +- 테넌트 자체를 권한의 최종 저장소로 사용하지 않음. +- 관리자 여부와 프로젝트 접근 권한은 내부 DB에서 최종 판정함. + +### 5.2 테넌트 기반 분기 규칙 + +| BARON-SSO 테넌트 상태 | 기본 사용자 분류 | 기본 이동 경로 | 추가 분기 | +| ----------------------- | ---------------------------------- | -------------------------------------- | --------------------------- | +| 관리자 테넌트 문맥 | 내부 DB 확인 후 관리자 또는 사용자 | 관리자 콘솔 후보 경로 또는 사용자 경로 | 내부 DB 권한으로 최종 판정 | +| 일반 사용자 테넌트 문맥 | 사용자 | 프로젝트별 피드백 작성 페이지 | 본인 글 목록/상세 접근 허용 | + +## 6. 로그인 후 페이지 분기 정책 + +### 6.1 관리자 후보 사용자 + +- 로그인 성공 후 내부 DB에서 프로젝트 관리자 권한이 확인되면 관리자 콘솔로 이동함. +- 기본 진입 후보 경로는 다음과 같음. +- `/main/project/[projectId]/feedback?channelId=[channelId]` +- 필요 시 운영 보조 화면 `/ops`, `/admin/issues` 로 이동 가능 +- 이후 프로젝트별 권한에 따라 진입 가능한 콘솔 페이지를 다시 제한함. + +### 6.2 일반 사용자 + +- 로그인 성공 후 각 소프트웨어 또는 서비스가 지정한 피드백 작성 페이지로 이동함. +- 진입 URL에는 프로젝트/채널에 대응되는 `workspaceCode` 또는 서비스 식별자가 함께 전달됨. +- 예시 +- `/support/EGBIM/new` +- `/support/TOVA/new` +- `/support/GAIA/new` +- `/support/KNGIL/new` +- `/support/INTRANET_QNA/new` + +## 7. 프로젝트 및 채널 초기 운영 구조 + +### 7.1 초기 프로젝트 생성 대상 + +| 프로젝트명 | 설명 | 최초 기본 채널 | +| -------------- | ----------------- | -------------- | +| `EGBIM` | EGBIM 전용 Q&A | `EGBIM` | +| `TOVA` | TOVA 전용 Q&A | `TOVA` | +| `GAIA` | GAIA 전용 Q&A | `GAIA` | +| `KNGIL` | KNGIL 전용 Q&A | `KNGIL` | +| `INTRANET_QNA` | 인트라넷 공통 Q&A | `INTRANET_QNA` | + +### 7.2 채널 운영 확장 계획 + +- 최초에는 프로젝트당 채널 1개로 시작함. +- 이후 각 프로젝트 내부에서 채널을 별도로 확장하여 문의 유형별로 분리 관리함. +- 예시 +- `EGBIM > 일반 문의`, `EGBIM > 장애 문의`, `EGBIM > 기능 개선` +- `INTRANET_QNA > 계정 문의`, `INTRANET_QNA > 권한 문의`, `INTRANET_QNA > 시스템 오류` + +## 8. 사용자 진입 구조 + +### 8.1 기본 시나리오 + +1. 사용자는 각 소프트웨어에서 이미 BARON-SSO 로그인 상태임. +2. 사용자가 Q&A 이동 버튼을 클릭함. +3. 소프트웨어는 자기 프로젝트에 대응되는 Q&A 진입 URL로 이동시킴. +4. 우리 시스템은 세션에서 SSO 식별자와 `tenant_id` 를 읽음. +5. 내부 DB에서 사용자와 프로젝트 관리자 권한을 조회함. +6. 일반 사용자면 해당 프로젝트의 작성 페이지로 이동함. +7. 프로젝트 관리자면 관리자 콘솔로 이동함. + +### 8.2 프로젝트별 사용자 진입 예시 + +| 진입 서비스 | 사용자 이동 경로 | 프로젝트 관리자 이동 경로 | +| ------------ | --------------------------- | -------------------------------------------------------------------------- | +| EGBIM | `/support/EGBIM/new` | `/main/project/[egbimProjectId]/feedback?channelId=[egbimChannelId]` | +| TOVA | `/support/TOVA/new` | `/main/project/[tovaProjectId]/feedback?channelId=[tovaChannelId]` | +| GAIA | `/support/GAIA/new` | `/main/project/[gaiaProjectId]/feedback?channelId=[gaiaChannelId]` | +| KNGIL | `/support/KNGIL/new` | `/main/project/[kngilProjectId]/feedback?channelId=[kngilChannelId]` | +| 인트라넷 Q&A | `/support/INTRANET_QNA/new` | `/main/project/[intranetProjectId]/feedback?channelId=[intranetChannelId]` | + +## 9. 문의 작성과 관리자 콘솔 분기 구조 + +### 9.1 기본 원칙 + +- 사용자는 프로젝트 안에서 글을 작성함. +- 글 작성 시 `문의 구분` 값을 함께 선택함. +- 글 작성 시 필요하면 `비밀글` 여부를 함께 선택함. +- 이 `문의 구분` 은 장기적으로 채널 또는 내부 `workspace` 분기 기준으로 사용함. +- 관리자 콘솔에서는 프로젝트 단위로 유입 건을 구분하여 관리하고, 비밀글은 권한 있는 관리자만 열람 가능하도록 제한함. + +### 9.2 현재 구조와 향후 확장 방향 + +| 항목 | 현재 기준 | 향후 확장 방향 | +| ----------- | ------------------------- | -------------------------------- | +| 프로젝트 | 서비스 단위 분리 | 유지 | +| 채널 | 프로젝트당 1개 기본 채널 | 필요 시 확장 | +| 문의 구분 | 사용자 작성 폼의 선택 값 | 운영 큐 분기 기준으로 사용 | +| 비밀글 | 작성 시 boolean 값 저장 | 역할별 조회 제한과 감사로그 추가 | +| 관리자 화면 | 프로젝트 단위 피드백 목록 | 프로젝트/문의구분 기반 다중 큐 | + +## 10. 현재 구현 구조와의 연결 기준 + +### 10.1 사용자 화면 + +- 현재 사용자용 지원 화면은 `apps/web/src/pages/support/**` 경로에 구성되어 있음. +- 사용자 상세에서는 관리자 이슈 상태, 댓글, 본인 글 수정/삭제를 제공함. +- 사용자 상세 상태 표시는 현재 ABC 원본 피드백의 연결 이슈 상태를 읽어 반영하도록 확장됨. + +### 10.2 관리자 화면 + +- 실제 관리자 콘솔은 `apps/web/src/pages/main/project/[projectId]/feedback.tsx` 경로를 중심으로 동작함. +- 피드백 상세 시트에서 댓글 CRUD와 이슈 연결 상태를 확인할 수 있도록 연계됨. +- 관리자 콘솔에서 이슈 연결 시 `apps/api` 와 `apps/secretary-api` 사이에서 내부 `support_tickets.issue_link_status` 를 함께 동기화하도록 보강됨. +- 관리자 권한 설정 페이지는 관리자 콘솔 메뉴 항목으로 추가하는 방향을 기준으로 함. + +### 10.3 제어 백엔드 + +- `apps/secretary-api` 는 사용자 화면용 상태, 댓글, 내부 티켓 메타데이터를 관리함. +- `apps/api` 는 ABC 관리자 콘솔의 피드백/이슈 기능을 제공함. +- 장기적으로는 BARON-SSO 로그인 완료 후 내부 DB 권한 조회 결과를 기준으로 사용자 경로와 관리자 경로를 라우팅하는 정책 계층이 추가되어야 함. + +## 11. 권한 처리 시퀀스 + +```mermaid +flowchart TD + A[사용자 또는 관리자\nBARON-SSO 로그인 상태] --> B[Q&A 이동 버튼 클릭] + B --> C[우리 시스템 진입] + C --> D[세션에서 SSO 식별자와 tenant_id 확인] + D --> E[내부 DB에서 사용자와 프로젝트 관리자 권한 조회] + E --> F{프로젝트 관리자 권한 존재?} + F -- 예 --> G[관리자 콘솔 진입] + F -- 아니오 --> H[사용자 작성 페이지 진입] + G --> I{프로젝트 접근 권한 존재?} + I -- 예 --> J[프로젝트별 관리자 콘솔 페이지 진입] + I -- 아니오 --> K[권한 없음 또는 다른 프로젝트로 재분기] + H --> L[프로젝트별 사용자 작성 페이지 이동] + L --> M[피드백 작성] + M --> N[문의 구분 값과 비밀글 여부 저장] + N --> O[프로젝트/문의구분 기준 관리자 콘솔 큐 반영] +``` + +## 12. 권한 매핑 테이블 초안 + +| 구분 | BARON-SSO 값 | 우리 시스템 해석 | 화면 권한 | 데이터 권한 | +| --------------- | ------------------------------ | -------------------------------------- | --------------------------------------------- | ------------- | +| 시스템 관리자 | 사용자 식별 정보 + tenant 문맥 | 내부 DB의 `SUPER_ADMIN` | 전체 관리자 콘솔, 설정, 프로젝트 관리 | 전체 프로젝트 | +| 프로젝트 관리자 | 사용자 식별 정보 + tenant 문맥 | 내부 DB의 `PROJECT_MANAGER` | 담당 프로젝트 콘솔, 권한 설정, 댓글/이슈 처리 | 배정 프로젝트 | +| 일반 사용자 | 사용자 식별 정보 + tenant 문맥 | 내부 DB 일반 사용자 또는 미승인 사용자 | 사용자 작성/목록/상세 | 본인 작성 글 | + +## 13. 구현 시 체크리스트 + +- BARON-SSO에서 내부 사용자 매핑에 사용할 식별 claim 확정 +- 프로젝트 `EGBIM`, `TOVA`, `GAIA`, `KNGIL`, `INTRANET_QNA` 생성 +- 각 프로젝트에 동일명 기본 채널 1개 생성 +- 프로젝트별 관리자 접근 정책 정의 +- 사용자 Q&A 이동 버튼의 프로젝트별 URL 매핑 정의 +- 로그인 후 내부 DB 권한 조회 기반 관리자/사용자 라우팅 구현 +- 문의 구분 값과 채널 분기 규칙 설계 +- 비밀글 저장, 조회 제한, 관리자 열람 규칙 설계 +- 프로젝트별 확장 채널 생성 전략 수립 + +## 14. 최종 정리 + +- BARON-SSO는 인증과 현재 테넌트 문맥 제공 시스템임. +- 최종 권한과 관리자 여부는 내부 DB가 결정함. +- 일반 사용자는 프로젝트별 피드백 작성 페이지로 이동함. +- 프로젝트 관리자는 담당 프로젝트의 관리자 콘솔로 이동함. +- 초기에는 프로젝트당 채널 1개와 프로젝트 관리자 역할만 두고, 이후 필요 시 채널 관리자와 세분화 권한을 확장함. +- 비밀글은 사용자 작성 기능과 관리자 조회 제한 정책에 포함해야 함. +- 현재 구현된 사용자 화면, 관리자 콘솔, 내부 티켓/댓글/이슈 상태 동기화 구조는 이 권한 설계 문서를 기준으로 다음 단계 SSO 연동과 내부 권한 구현으로 연결할 수 있음. diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks.md b/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks.md new file mode 100644 index 0000000..a68f2c5 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks.md @@ -0,0 +1,499 @@ +# 사내 지원 플랫폼 환경 셋업 및 Task 목록 + +## 1. 문서 목적 + +본 문서는 [architecture_secretary_sso_user_scenarios.md](./architecture_secretary_sso_user_scenarios.md) 에서 정리한 사용자 시나리오와 시스템 역할 분담을 실제 개발 환경으로 옮기기 위한 실행 계획 문서임. + +권한, 테넌트, 로그인 후 페이지 분기 설계는 [architecture_secretary_sso_role_access.md](./architecture_secretary_sso_role_access.md) 에 별도 정리함. + +핵심 목적은 다음과 같음. + +- 어떤 순서로 환경을 셋업해야 하는지 명확히 정리함. +- 개발 착수 전에 필요한 선행 정보와 의존 항목을 체크함. +- 사용자 화면, 운영 화면, 관리자 기능, 외부 연동까지 단계별로 작업을 분해함. +- 시연과 운영 전환을 위한 검증 항목을 체크리스트로 관리함. + +## 2. 기본 원칙 + +- 먼저 ABC User Feedback 기본 환경을 안정적으로 띄움. +- 그 다음 BARON-SSO, 우리 시스템 백엔드, 자체 DB를 연결함. +- 이후 사용자 화면, 운영 화면, 관리자 기능 연계를 붙임. +- 마지막으로 시연 기준 end-to-end 검증을 수행함. + +## 2.1 현재 작업 기준선 + +- 프로젝트/채널 구조 확정과 BARON-SSO `tenant_id` 기반 권한 분기 정책은 최종 완료의 선행 조건으로 유지함. +- 다만 현재 주차에서는 위 선행 조건이 아직 확정되지 않았으므로, 우선 범위를 `프론트 UI 구성 + 우리 시스템 백엔드/DB 구축`까지로 제한함. +- 다음 주 작업 범위는 `BARON-SSO 로그인 연동 + 권한 분기 + 페이지 분기 처리`로 계획함. +- 따라서 현재 문서의 `(완료)` 표시는 SSO/권한 확정 이전에도 독립적으로 검증 가능한 UI, 라우팅, 스텁 API, DB 스키마, 로컬 실행 항목에만 부여함. + +## 3. 전체 Task 로드맵 + +| 단계 | 작업 묶음 | 핵심 목표 | 완료 기준 | +| --- | --- | --- | --- | +| 1 | 로컬 기본 환경 구성 | ABC, DB, 개발 도구를 실행 가능한 상태로 만듦 | 로컬에서 ABC Web/API 접속 가능 | +| 2 | ABC 운영 구조 셋업 | 프로젝트, 채널, 필드, 역할 구조를 준비함 | 서비스별 채널/필드/권한 정책 초안 반영 완료 | +| 3 | BARON-SSO 연동 준비 | 로그인과 사용자 식별 체계를 연결함 | `user_id`, `tenant_id`, 역할 정보를 세션에서 읽을 수 있음 | +| 4 | 우리 시스템 백엔드/DB 셋업 | 내부 티켓/승인/매핑 저장소를 준비함 | `support_tickets`, `request_approvals`, `abc_feedback_mappings` 저장 가능 | +| 5 | 사용자 화면 셋업 | 작성, 목록, 상세 흐름을 연결함 | 사용자 기준 등록/조회 시연 가능 | +| 6 | 운영/관리 화면 셋업 | 승인, 이슈 생성, 처리 결과 입력 흐름을 연결함 | 운영자/관리자 기준 처리 시연 가능 | +| 7 | 외부 연동/알림 준비 | 알림 및 업무 시스템 연계를 준비함 | Mock 또는 실제 연동 경로 확인 완료 | +| 8 | 통합 검증/시연 준비 | 전체 흐름을 점검하고 시연용 데이터를 고정함 | end-to-end 시연 체크리스트 통과 | + +## 4. 단계별 상세 Task + +### 4.1 로컬 기본 환경 구성 + +- 작업 기준 저장소: `abcfeedback_test` +- 현재 확인된 로컬 기동 방식: `./start-local.sh` 또는 `docker compose -f docker/docker-compose.yml up -d` +- 선행 확인 항목 +- Docker CLI 설치 및 Docker daemon 접근 가능 여부 확인 +- Node.js / pnpm 설치 여부 확인 +- 로컬 포트 사용 여부 확인: `3001`, `4000`, `5080`, `13306` +- 현재 확인된 기본 접속 URL +- Web UI: `http://localhost:3001` +- API Docs: `http://localhost:4000/docs` +- API Health: `http://localhost:4000/api/health` +- SMTP Test Inbox: `http://localhost:5080` +- 현재 확인된 기본 DB 접속 정보 +- DB Engine: MySQL 8.0 +- Host: `localhost` +- Port: `13306` +- Database: `userfeedback` +- Username: `userfeedback` +- Password: `userfeedback` +- 현재 확인된 핵심 환경 변수 +- Web: `NEXT_PUBLIC_API_BASE_URL=http://localhost:4000` +- API: `JWT_SECRET`, `MYSQL_PRIMARY_URL`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_SENDER` +- 1차 실행 절차 +- `cd abcfeedback_test` +- `./start-local.sh` `(완료)` +- `./check-local.sh` `(완료)` +- 기동 후 첫 진입 절차 +- 테넌트 생성 +- 관리자 계정 생성 +- 로그인 +- Project 생성 +- Channel 생성 +- API Key 생성 +- 첫 피드백 등록 +- 현재 작업 환경 확인 결과 +- Docker daemon 기동 후 `start-local.sh`, `check-local.sh` 기준 로컬 컨테이너 실행과 기본 헬스체크 확인 `(완료)` +- Web `3001`, API `4000`, SMTP UI `5080`, MySQL `13306` 포트 매핑 확인 `(완료)` +- 산출물 +- 로컬 실행 스크립트 및 접속 URL 문서화 완료 +- 4.2 진행 전 선행 조건: ABC Web/API Health check 성공 확인 + +### 4.2 ABC 운영 구조 셋업 + +- 설계 기준 문서 +- `docs/architecture_secretary_sso_components_v2.md` +- `docs/architecture_secretary_sso_user_scenarios.md` +- 운영 구조 설계 원칙 +- ABC Project는 서비스 운영 단위로 생성하고, 세부 신청/문의 유형은 Channel로 분리함 +- 외부 진입 식별자 `app_id`, `service_type_id`, 메뉴 코드는 우리 시스템에서 `workspace`로 해석하고, `workspace_channel_mappings`로 ABC Channel에 연결함 +- 일반 사용자 입력 스키마는 우리 시스템 동적 폼과 ABC 커스텀 필드를 동시에 맞춰야 하므로 `workspace_field_mappings` 기준으로 관리함 +- 역할과 화면 권한은 BARON-SSO 세션을 기준으로 판정하되, ABC 프로젝트 접근 권한은 별도로 부여함 +- 1차 운영 구조 초안 +- Project 단위 +- `INTRANET_SUPPORT`: 인트라넷 기반 사내 지원 업무 공통 Project +- `SOFTWARE_QA`: S/W 프로그램별 문의 접수 공통 Project +- Channel 단위 +- `INTRA_SUPPLIES_REQUEST`: 물품 신청 +- `INTRA_BOOK_REQUEST`: 도서 신청 +- `INTRA_VEHICLE_REQUEST`: 출장 차량 신청 +- `INTRA_EQUIPMENT_RENTAL`: 비품 대여 +- `INTRA_GENERAL_QNA`: 사내 문의 +- `SW_APP__QNA`: 앱별 전용 Q&A 채널 +- Field 스키마 초안 +- 공통 필드: `request_category`, `title`, `description`, `requester_contact`, `attachment` +- 승인형 업무 필드: `approval_required`, `approver_org`, `requested_date`, `priority` +- 자산/물품형 필드: `asset_type`, `quantity`, `usage_purpose`, `delivery_location` +- 도서형 필드: `book_title`, `author`, `publisher`, `purchase_reason` +- 차량형 필드: `departure_date`, `return_date`, `destination`, `passenger_count` +- Q&A형 필드: `app_version`, `environment`, `error_message`, `expected_result` +- 역할 정의 초안 +- `ROLE_USER`: 일반 사용자 작성/본인 조회 +- `ROLE_APPROVER`: 승인 대기 조회, 승인/반려 처리 +- `ROLE_OPERATOR`: 전체 운영 목록 조회, 상태 변경, 이슈 연결 +- `ROLE_ADMIN`: Project/Channel/Field/Member 관리 +- 멤버/권한 부여 정책 +- Project 멤버는 최소 `승인자`, `운영 담당자`, `관리자` 그룹으로 시작함 +- Channel 단위 운영은 가능하면 Project Role로 통일하고, 예외 채널만 추가 권한을 부여함 +- 운영 화면 접근 권한과 ABC 관리자 권한은 동일 인원 기준으로 시작하되, 이후 분리 가능하게 설계함 +- 1차 실무 작업 순서 +- `INTRANET_SUPPORT`, `SOFTWARE_QA` Project 생성 +- 인트라넷용 기본 Channel 5종 생성 +- 시범 앱 1개를 선정해 `SW_APP__QNA` Channel 생성 +- Channel별 필드 스키마 반영 +- Project Role 생성 및 멤버 배정 +- `workspace_channel_mappings`, `workspace_field_mappings` 초안 작성 +- seed 데이터 준비 항목 +- 테스트용 일반 사용자 1명, 승인자 1명, 운영 담당자 1명, 관리자 1명 +- 인트라넷 업무 5종에 대한 샘플 Channel/Field 정의서 +- 앱 Q&A 1종에 대한 샘플 Channel/Field 정의서 +- `workspace` 매핑 초안 표 + +| workspace_code | 진입 구분 | ABC Project | ABC Channel | 주요 Field 그룹 | 승인 필요 기본값 | +| --- | --- | --- | --- | --- | --- | +| `INTRA_SUPPLIES_REQUEST` | 인트라넷 물품 신청 | `INTRANET_SUPPORT` | `INTRA_SUPPLIES_REQUEST` | 공통 + 승인형 + 자산/물품형 | 예 | +| `INTRA_BOOK_REQUEST` | 인트라넷 도서 신청 | `INTRANET_SUPPORT` | `INTRA_BOOK_REQUEST` | 공통 + 승인형 + 도서형 | 예 | +| `INTRA_VEHICLE_REQUEST` | 인트라넷 차량 신청 | `INTRANET_SUPPORT` | `INTRA_VEHICLE_REQUEST` | 공통 + 승인형 + 차량형 | 예 | +| `INTRA_EQUIPMENT_RENTAL` | 인트라넷 비품 대여 | `INTRANET_SUPPORT` | `INTRA_EQUIPMENT_RENTAL` | 공통 + 승인형 + 자산/물품형 | 예 | +| `INTRA_GENERAL_QNA` | 인트라넷 일반 문의 | `INTRANET_SUPPORT` | `INTRA_GENERAL_QNA` | 공통 + Q&A형 | 아니오 | +| `SW_APP__QNA` | S/W 프로그램별 문의 | `SOFTWARE_QA` | `SW_APP__QNA` | 공통 + Q&A형 | 아니오 | + +- `workspace_field_mappings` 작성 규칙 초안 +- 모든 `workspace`는 공통 필드 `request_category`, `title`, `description`, `requester_contact`, `attachment`를 기본 포함함 +- 승인형 업무는 `approval_required=true`를 기본값으로 두고, 승인자 조직/우선순위/희망일자를 추가 매핑함 +- Q&A형 업무는 승인 필드 대신 `app_version`, `environment`, `error_message`, `expected_result`를 우선 매핑함 +- ABC 커스텀 필드 키는 가능하면 `snake_case`로 고정하고, 우리 시스템 `local_field_code`와 동일한 이름을 사용함 +- 첨부는 ABC 첨부 기능을 기본 저장소로 사용하고, 우리 시스템에는 첨부 메타데이터와 내부 티켓 연결 정보만 저장함 +- 완료 기준 +- 서비스별 Project/Channel/Field/Role 초안이 문서로 정리되어 있어야 함 +- 최소 1개 인트라넷 업무와 1개 S/W 앱 Q&A에 대해 실제 ABC 생성 대상 목록이 확정되어 있어야 함 +- 현재 메모 +- 이 단계는 프론트 UI/백엔드 스텁 완료와 별개로 아직 진행중이며, Project/Channel 구조가 확정되어야 최종 완료 처리 가능함 + +### 4.3 BARON-SSO 연동 준비 + +- 확인된 전제 +- ABC User Feedback는 커스텀 OAuth 2.0 / OIDC 제공자 연동을 지원함 +- 현재 ABC Web의 OAuth 콜백 경로는 `/auth/oauth-callback` 기준으로 동작함 +- 현재 Web 외부 노출 포트는 로컬 기준 `3001`이므로, ABC 관리자 UI를 그대로 사용할 경우 개발용 콜백 URI 후보는 `http://localhost:3001/auth/oauth-callback` 임 +- 확보해야 할 BARON-SSO 클라이언트 등록 정보 +- `client_id` +- `client_secret` +- `authorization_endpoint` +- `token_endpoint` +- `userinfo_endpoint` +- 가능하면 `jwks_uri` +- `issuer` +- 지원 `scope` 목록: 최소 `openid`, 사용자 식별, 이메일, 조직/역할 관련 scope +- Redirect / Callback / Logout URI 정리 항목 +- ABC 관리자 UI OAuth Callback URI: `/auth/oauth-callback` +- 우리 시스템 사용자 포털 Callback URI: 별도 Next.js 앱 경로 확정 필요 +- 우리 시스템 운영 포털 Callback URI: 사용자 포털과 분리 여부 확정 필요 +- Logout Redirect URI: BARON-SSO 로그아웃 후 복귀할 공통 URL 확정 필요 +- 운영 환경별로 `local`, `dev`, `stg`, `prod` URI를 각각 등록 목록으로 정리해야 함 +- 세션 저장 구조 초안 +- 인증 원본은 BARON-SSO가 담당하고, 우리 시스템은 세션 또는 JWT에 필요한 최소 클레임만 저장함 +- 공통 세션 필드: `user_id`, `tenant_id`, `display_name`, `email`, `role_keys`, `workspace_scopes`, `access_token_exp`, `refresh_token_exp` +- 선택 세션 필드: `org_id`, `org_name`, `employee_no`, `position_code` +- 서버 측 권한 판정은 세션에 저장된 역할 문자열만 신뢰하지 않고, `tenant_id + workspace + 내부 권한 테이블`을 함께 사용함 +- 역할 판정 기준 초안 +- `ROLE_USER`: 본인 작성/조회만 가능 +- `ROLE_APPROVER`: 승인 대기 목록, 승인/반려 가능 +- `ROLE_OPERATOR`: 운영 목록, 상태 변경, 이슈 연결 가능 +- `ROLE_ADMIN`: Project/Channel/Field/Member 관리 가능 +- 역할 판정 순서 +- 1차: BARON-SSO 토큰 또는 userinfo에서 조직/그룹/역할 claim 확인 +- 2차: 우리 시스템 `user_workspace_access` 기준으로 `workspace`별 실제 권한 확정 +- 3차: ABC 프로젝트 멤버십과 운영 화면 접근 권한 동기화 +- 예외 처리 정책 +- 미로그인: SSO 로그인 페이지로 리다이렉트 +- 세션 만료: 재로그인 유도 후 원래 진입 경로 복귀 +- 권한 부족: 403 화면 + 요청 가능한 담당 조직 안내 +- `workspace` 매핑 없음: 일반 오류가 아니라 운영 설정 누락으로 분류하여 관리자 알림 대상에 포함 +- BARON-SSO userinfo 조회 실패: 재시도 1회 후 실패 로그 적재 및 운영 알림 +- 1차 실무 작업 순서 +- BARON-SSO 담당자에게 클라이언트 등록 요청 템플릿 전달 +- 환경별 Redirect/Callback/Logout URI 목록 확정 +- 토큰 claim 샘플 수집: `user_id`, `tenant_id`, 조직/역할 관련 필드 확인 +- 세션 스키마와 내부 권한 매핑 규칙 정의 +- 테스트 계정 준비: 일반 사용자, 승인자, 운영 담당자, 관리자 +- 로그인 성공/권한 부족/세션 만료/`workspace` 미매핑 시나리오 검증 항목 작성 +- 완료 기준 +- BARON-SSO 클라이언트 등록에 필요한 입력값 목록이 문서화되어 있어야 함 +- 세션 저장 필드와 역할 판정 규칙이 문서화되어 있어야 함 +- 개발 환경 기준 Callback URI와 예외 처리 정책이 확정되어 있어야 함 +- 현재 메모 +- BARON-SSO 로그인과 `tenant_id` 기반 권한/페이지 분기 처리는 다음 주 구현 범위로 계획함 + +### 4.4 우리 시스템 백엔드/DB 셋업 + +- 구현 전 정합성 확인 +- 4.4 기준 DB 엔진은 MySQL 8.0으로 고정함 +- `architecture_secretary_sso_components_v2.md`의 SQL 초안에는 `SERIAL`, `JSONB` 등 PostgreSQL 스타일 표현이 포함되어 있으므로 실제 구현 전 MySQL 문법으로 변환해야 함 +- FastAPI 기본 구조 초안 +- 실제 생성 경로: `apps/secretary-api` +- `apps/secretary-api/app/api`: 사용자/운영 API 라우터 +- `apps/secretary-api/app/core`: 설정, 보안, 세션, 공통 유틸 +- `apps/secretary-api/app/models`: SQLAlchemy 모델 또는 ORM 엔티티 +- `apps/secretary-api/app/schemas`: 요청/응답 DTO +- `apps/secretary-api/app/services`: 권한 판정, 티켓 처리, 승인 처리, ABC 브릿지 +- `apps/secretary-api/app/repositories`: DB 접근 계층 +- `apps/secretary-api/app/integrations/abc`: ABC API 클라이언트 +- `apps/secretary-api/app/integrations/sso`: BARON-SSO 토큰 검증 또는 userinfo 연동 +- `apps/secretary-api/alembic`: 마이그레이션 파일 +- `apps/secretary-api/app/seeds`: 코드성 초기 데이터 및 테스트용 seed 스크립트 +- 현재 저장소 기준 확인 결과 +- `apps/secretary-api/app/main.py`에 FastAPI 엔트리포인트가 존재함 +- `apps/secretary-api/app/api/routes/health.py`에 `/api/health` 엔드포인트가 존재함 +- `apps/secretary-api/app/api/routes/tickets.py`에 `GET /api/workspaces`, `GET /api/workspaces/{workspace_code}/form-template`, `POST /api/workspaces/{workspace_code}/tickets`, `GET /api/workspaces/{workspace_code}/tickets`, `GET /api/tickets/{ticket_id}`, `POST /api/tickets/{ticket_id}/approve`, `POST /api/tickets/{ticket_id}/issue` 경로가 존재함 `(완료)` +- `apps/secretary-api/alembic/versions/0001_initial_support_schema.py`에 P0 기준 1차 MySQL 마이그레이션이 존재함 +- `apps/secretary-api/alembic/versions/0002_support_ticket_content_fields.py`에 현재 UI가 사용하는 `description`, `approval_status`, `sync_status`, `issue_link_status`, `extra_fields` 컬럼 보강이 반영됨 `(완료)` +- 현재 작업 환경에서는 `localhost:13306` 대신 Docker bridge IP 기준 `DATABASE_URL` override 로 실제 MySQL 마이그레이션과 티켓 생성/조회 검증을 완료함 `(부분 완료)` +- 다만 ABC 연동 클라이언트, seed 스크립트 정리, 환경 공통 DB 접속 방식 정리는 아직 후속 작업으로 남아 있음 +- DB 구축 순서 +- 1단계: 코드 테이블 및 서비스 마스터 생성 +- `support_status_codes` +- `support_category_codes` +- `software_apps` +- `service_types` +- `workspaces` +- 2단계: 권한 및 채널 매핑 테이블 생성 +- `user_workspace_access` +- `workspace_channel_mappings` +- `workspace_field_mappings` +- 3단계: 핵심 티켓 및 연동 테이블 생성 +- `support_tickets` +- `abc_feedback_mappings` +- `request_approvals` +- `notification_logs` +- `operator_histories` 또는 `ticket_activity_logs` +- 4단계: 확장 업무 테이블 생성 +- `assets` +- `asset_allocations` +- `vehicle_schedules` +- `remote_support` +- 5단계: 이관 추적 테이블 생성 +- `migration_batches` +- `migration_mappings` +- 핵심 테이블 구현 우선순위 +- P0: `workspaces`, `workspace_channel_mappings`, `workspace_field_mappings` +- P0: `support_tickets`, `abc_feedback_mappings` +- P0: `user_workspace_access`, `request_approvals` +- P1: `notification_logs`, `ticket_activity_logs` +- P1: `migration_batches`, `migration_mappings` +- P2: `assets`, `asset_allocations`, `vehicle_schedules`, `remote_support` +- MySQL 변환 규칙 초안 +- `SERIAL`은 `BIGINT AUTO_INCREMENT` 또는 `INT AUTO_INCREMENT`로 변환 +- `JSONB`는 MySQL `JSON`으로 변환 +- `BOOLEAN`은 MySQL `BOOLEAN` 또는 `TINYINT(1)` 기준으로 통일 +- `TIMESTAMP DEFAULT CURRENT_TIMESTAMP`와 `updated_at` 자동 갱신 규칙을 명시적으로 넣음 +- 코드 테이블과 상태값은 가능하면 `ENUM`보다 참조 테이블 방식을 우선 적용함 +- 마이그레이션 도구 및 seed 준비 +- Alembic 기반 버전 관리 적용 +- 최초 마이그레이션은 서비스 마스터, 권한, 채널 매핑, 핵심 티켓 테이블까지 포함 +- seed 1차 범위: 상태 코드, 카테고리 코드, 기본 `workspace`, 기본 Project/Channel 매핑, 테스트 사용자 권한 +- seed 2차 범위: 샘플 티켓, 샘플 승인 데이터, 샘플 알림 이력 +- ABC API 연동용 서비스 계층 초안 +- `ABCProjectService`: Project/Channel 조회 및 캐시 +- `ABCFeedbackService`: 피드백 생성, 상세 조회, 첨부 업로드 +- `ABCIssueService`: 이슈 생성, 피드백-이슈 연결 +- `ABCMemberSyncService`: 운영자/승인자 프로젝트 멤버십 동기화 +- 1차 실무 작업 순서 +- `apps/secretary-api` FastAPI 프로젝트 뼈대 생성 +- MySQL 연결 설정 및 로컬 `.env` 정의 +- Alembic 초기화 및 1차 마이그레이션 작성 +- 코드 테이블 및 `workspace` 관련 테이블 생성 +- `support_tickets`, `abc_feedback_mappings`, `request_approvals` 생성 +- 상태 코드 및 `workspace` seed 적재 +- `GET /api/workspaces`, `POST /api/workspaces/{workspaceCode}/tickets` 스텁 API 구현 +- ABC API 클라이언트 골격 구현 +- 티켓 생성 시 ABC 저장 후 내부 매핑 저장하는 트랜잭션 흐름 설계 +- 완료 기준 +- MySQL 기준 1차 마이그레이션이 적용 가능해야 함 +- `workspace`와 ABC Channel 간 기본 매핑 데이터가 적재 가능해야 함 +- 내부 티켓 생성과 ABC `feedback_id` 매핑 저장 구조가 확정되어 있어야 함 +- 승인 이력 저장 구조와 운영 이력 저장 구조가 확정되어 있어야 함 +- 현재 메모 +- `baron_support` DB에 실제 마이그레이션 적용과 티켓 생성/목록 조회는 검증했으며, 현재 프런트 `/support` 흐름과 연결되는 최소 실DB 경로는 확보된 상태임 +- 추가 검증 메모 +- 현재 `apps/web` fallback 경로 기준으로 ABC MySQL `userfeedback.feedbacks` 저장 및 관리자 Feedback 화면 조회까지 검증했음 +- `workspaceCode`별로 ABC Channel 분기 저장되며, 채널 필드 스키마 기준으로 `title`/`contents` 또는 `message` 필드값만 저장되도록 정리했음 +- 다만 이 문서 기준 완료 조건인 내부 `support_tickets` + `abc_feedback_mappings` 동시 저장을 현재 fallback 경로가 모두 대체하는 것은 아니므로, ABC DB 기준으로는 `Create/Read` 검증 완료, 전체 CRUD 완료로 보기는 이름 + +### 4.5 사용자 화면 셋업 + +- 현재 저장소 기준 확인 결과 +- 실제 서비스 웹앱은 `apps/web/src` 기준으로 운영되고 있음 +- 현재 `apps/web/src/pages/support/[workspaceCode]/new|list|[ticketId]|index` 라우트가 존재함 `(완료)` +- 사용자 화면은 실제 서비스 라우트 기준으로 운영되며, 로컬 dev 서버 `3002`에서 시연 가능함 `(완료)` +- 단, `docker compose`만으로는 `3002`가 열리지 않으며 `apps/web`를 별도 실행해야 함 +- 예시: `cd apps/web && PORT=3002 pnpm dev` +- 라우팅 구조 초안 +- `/support` 또는 `/portal/support`: 공통 진입 라우터 +- `/support/:workspaceCode/new`: 사용자 작성 화면 `(완료)` +- `/support/:workspaceCode/list`: 내 접수 목록 `(완료)` +- `/support/:workspaceCode/:ticketId`: 접수 상세 `(완료)` +- `/support/:workspaceCode`: 기본 진입 시 목록으로 리다이렉트 `(완료)` +- 승인형 업무와 Q&A형 업무는 같은 라우팅 패턴을 사용하되, `workspaceCode`에 따라 폼 템플릿과 후처리만 다르게 적용함 +- 공통 로그인 후 화면 분기 규칙 +- SSO 로그인 완료 후 진입 파라미터 `app_id`, `service_type_id`, 메뉴 코드 중 하나를 받아 `workspaceCode`로 해석함 +- `workspaceCode`가 확정되면 `GET /api/workspaces/{workspaceCode}/form-template`로 필드 구성을 조회함 +- 권한이 없으면 작성 화면으로 보내지 않고 403 또는 접근 안내 화면으로 분기함 +- 사용자 피드백 작성 페이지 구현 항목 +- 공통 입력 필드: 제목, 내용, 첨부 `(완료)` +- `workspace`별 동적 필드 렌더링: 현재는 최소 Q&A 작성형으로 단순화하여 일부만 사용 `(부분 완료)` +- 제출 전 유효성 검사: 필수값, 날짜 범위, 숫자 범위, 첨부 제한 +- 제출 시 처리 순서: 우리 시스템 API 호출 -> ABC 저장 -> 내부 `support_tickets` 생성 -> `abc_feedback_mappings` 저장 +- 현재 검증 상태: `apps/web` fallback 경로에서 ABC `feedbacks` 저장과 관리자 조회는 확인했으며 `(부분 완료)`, 내부 `support_tickets`/`abc_feedback_mappings`까지 같은 요청에서 항상 저장되는 구조는 secretary-api 기준으로 계속 검증 필요 +- 완료 화면에는 `ticket_id`, `feedback_id`, 현재 상태값을 함께 보여줌 +- 내 피드백 목록 페이지 구현 항목 +- 로그인 사용자 기준 `requester_id + requester_tenant_id` 필터 적용 `(완료)` +- 목록 컬럼 초안: 제목, 요청 유형, 작성일, 내부 상태 중심 게시판 형태 구현 `(완료)` +- `workspace` 필터와 상태 필터 제공 +- 상단 우측 검색 입력 및 행 전체 클릭 상세 이동 구현 `(완료)` +- 리스트 하단 좌측 작성 페이지 이동 버튼 구현 `(완료)` +- 피드백 상세 페이지 구현 항목 +- ABC 원문 본문/첨부/댓글 조회 영역 +- 우리 시스템 상태 메타데이터 영역: 현재는 최소 상태 배지와 댓글 입력 영역 중심으로 단순화 `(부분 완료)` +- 승인형 업무는 승인 이력 타임라인 노출 +- 일반 Q&A형 업무는 이슈 연결 상태와 답변 상태를 노출 +- `workspace`별 채널/필드 자동 선택 로직 +- `app_id`, `service_type_id`, 메뉴 코드 -> `workspaceCode` 해석 +- `workspaceCode` -> `workspace_channel_mappings`로 ABC Channel 결정 +- `workspaceCode` -> `workspace_field_mappings`로 폼 필드와 검증 규칙 로드 +- 채널 매핑이 없으면 작성 화면 진입 전 관리자 설정 누락 오류로 처리 +- 작성 완료 후 매핑 저장 처리 +- 우리 시스템은 ABC API 응답의 `feedback_id`를 수신한 뒤 같은 요청 컨텍스트에서 내부 티켓과 1:1 매핑 저장 +- 저장 실패 시 사용자에게는 접수 보류 상태를 안내하고, 운영자에게 재처리 대상 알림을 남김 +- 사용자 조회 화면의 상태 노출 원칙 +- ABC 상태값이 아니라 우리 시스템 `support_tickets.status_code`를 주 상태값으로 사용함 +- 보조 상태로 `approval_status`, `sync_status`, `issue_link_status`를 함께 노출함 +- 사용자는 본인 데이터만 조회 가능하고, `tenant_id` 불일치 데이터는 절대 노출하지 않음 +- 1차 실무 작업 순서 +- 사용자 공통 진입 URL 규칙 확정 +- `workspace` 해석 API 또는 미들웨어 구현 +- 폼 템플릿 조회 API 구현 `(완료 - 스텁 기준)` +- 작성 API와 목록 API 구현 `(완료 - 현재 secretary-api 연동 기준)` +- 상세 API 구현: 내부 상태 + ABC 원문 조합 응답 `(부분 완료)` +- 샘플 `workspace` 1종으로 작성 -> 목록 -> 상세 흐름 검증 `(완료)` +- 완료 기준 +- 최소 1개 `workspace`에서 작성, 목록, 상세 흐름이 연결되어 있어야 함 +- 작성 완료 시 `support_tickets`와 `abc_feedback_mappings`가 함께 생성되어야 함 +- 현재 확인 범위: ABC `feedbacks` 저장과 관리자 조회까지는 확인 완료, 내부 매핑 저장은 secretary-api 경로 기준 완료 메모를 유지하되 web fallback 경로는 별도 검증 필요 +- 목록/상세 화면에서 내부 상태값이 ABC 원문과 함께 노출되어야 함 + +### 4.6 운영/관리 화면 셋업 + +- 현재 저장소 기준 확인 결과 +- 실제 서비스 웹앱 `apps/web/src/pages` 이하에 `/ops`, `/admin/issues` 라우트가 존재함 `(완료)` +- 운영/관리 화면은 실제 페이지로 1차 이관되었고 승인/이슈 생성 흐름은 현재 secretary-api 및 내부 DB 경로 기준으로 계속 확장 중임 `(부분 완료)` +- 운영 화면 라우팅 구조 초안 +- `/ops/approvals`: 승인 대기 목록 +- `/ops/tickets`: 운영 전체 목록 +- `/ops/tickets/:ticketId`: 운영 상세 +- `/admin/issues`: 이슈 생성 대상 목록 +- `/admin/issues/:ticketId`: 피드백-이슈 연결 상세 +- 승인 대상 조회 및 승인/반려 처리 구현 항목 +- 조회 조건: `request_approvals.approval_status = PENDING` 또는 `support_tickets.requires_approval = true and status_code = RECEIVED` +- 승인 화면 컬럼 초안: 제목, 요청 유형, 요청자, 요청일시, 현재 상태, 승인 필요 사유 +- 승인 시 처리 순서: 권한 검증 -> `request_approvals` 기록 -> `support_tickets.status_code` 갱신 -> 알림 이벤트 발행 +- 반려 시 처리 순서: 반려 사유 저장 -> 사용자 노출 상태 갱신 -> 알림 이벤트 발행 +- ABC 관리자 UI 진입 링크 또는 연동 포인트 +- 운영 상세에서 대응 ABC 원문 링크와 관리자 UI 바로가기 제공 +- 관리자 화면에서는 `abc_feedback_id`, `abc_channel_id`, `abc_feedback_url`을 함께 노출 +- 필요 시 ABC 기본 목록/상세 UI를 새 탭으로 열고, 우리 시스템은 상태 제어와 처리 이력만 담당함 +- 피드백-이슈 연결 흐름 구현 항목 +- 승인 완료 또는 운영 판단 완료 상태의 티켓만 이슈 생성 가능 +- 이슈 생성 시 ABC 관리자 API 호출 후 `issue_id`와 연결 시각을 내부 이력에 저장 +- 이미 연결된 티켓은 중복 생성이 아니라 기존 이슈 링크로 유도 +- 연결 실패 시 내부 상태는 유지하고 `sync_status` 또는 운영 이력에 실패 원인을 저장 +- 담당자 배정, 처리 메모, 상태 전이 로직 +- 상태 전이 초안: `RECEIVED -> PENDING_APPROVAL -> APPROVED -> IN_PROGRESS -> RESOLVED -> CLOSED` +- 반려 흐름 초안: `PENDING_APPROVAL -> REJECTED` +- 담당자 배정은 `current_assignee_id`, `current_assignee_tenant_id`에 기록 +- 처리 메모와 상태 변경 이력은 `operator_histories` 또는 `ticket_activity_logs`에 누적 저장 +- 처리 결과/답변 입력 후 사용자 노출 데이터 동기화 +- 내부 상태 변경 시 사용자 상세 화면의 상태 배지와 최근 처리 결과를 즉시 반영 +- 필요 시 ABC 원문 댓글 또는 이슈 상태와 최소 동기화 규칙을 정의 +- 사용자가 보는 최종 상태는 우리 시스템 `support_tickets.status_code`를 기준으로 유지 +- 역할별 접근 제한 검증 항목 +- 승인자: 승인 대기 목록과 승인/반려만 가능, 전체 운영 설정 변경 불가 +- 운영 담당자: 전체 운영 목록, 상태 변경, 담당자 배정, 처리 메모 가능 +- 관리자: Project/Channel/Member/이슈 연결 관리 가능 +- 일반 사용자: 운영/관리 URL 직접 접근 시 403 처리 +- 1차 실무 작업 순서 +- 승인 대기 목록 API 구현 `(완료 - 현재 preview 흐름 기준)` +- 승인/반려 API 구현 및 `request_approvals` 저장 `(부분 완료 - 현재 내부 상태 전이 기준)` +- 운영 상세 API 구현: 내부 상태 + ABC 원문 링크 조합 +- 담당자 배정 및 처리 메모 API 구현 +- 이슈 생성 및 피드백-이슈 연결 API 구현 `(부분 완료 - 현재 secretary-api 연동 기준)` +- 운영/관리 화면을 실제 라우트 구조로 정리 `(완료)` +- 승인 -> 이슈 생성 -> 사용자 상세 반영 시나리오 검증 `(부분 완료)` +- 완료 기준 +- 승인자가 승인/반려를 수행하면 내부 승인 이력과 티켓 상태가 함께 갱신되어야 함 +- 운영 담당자가 상태 변경과 처리 메모를 남길 수 있어야 함 +- 관리자 화면에서 승인 완료 티켓을 이슈 생성 대상으로 식별하고 연결할 수 있어야 함 + +### 4.7 외부 연동/알림 준비 + +- 대상 연동 범위 초안 +- 메일 알림 +- 사내 메신저 또는 협업 도구 알림 +- 후속 업무 시스템 연계 +- 운영 설정 누락 및 동기화 실패 감지 +- 이벤트 기준 초안 +- 접수 완료: 요청 제목, 접수 번호, 상세 링크 +- 승인 대기: 승인 대상자, 요청 요약, 승인 링크 +- 승인 완료: 요청 제목, 승인 결과, 다음 단계, 상세 링크 +- 반려 완료: 반려 사유, 재작성 또는 문의 안내 +- 처리 완료: 처리 결과 요약, 추가 확인 필요 여부, 상세 링크 +- `RESOLVED`: 처리 완료 결과 알림 +- 자산/차량 연동형 업무는 승인 완료 후 후속 처리 이벤트를 별도 발행함 +- 구현 준비 항목 +- 알림 채널별 템플릿 정의 +- 이벤트 발생 시점 정의 +- 재시도 및 실패 로그 정책 정의 +- 민감 정보 마스킹 정책 정의 +- 완료 기준 +- 승인 완료/처리 완료 이벤트 기준 알림 발송 검증 +- Mock 또는 실제 연동 경로가 최소 1개 이상 확인되어야 함 + +### 4.8 통합 검증/시연 준비 + +- 시연용 고정 데이터 준비 +- 샘플 접수 데이터 3종: 승인형 1건, 일반 Q&A 1건, 처리완료 1건 +- 테스트 계정: 일반 사용자 1명, 승인자 1명, 운영 담당자 1명, 관리자 1명 +- 체크리스트형 실행 순서 +- 일반 사용자 로그인 또는 진입 +- 작성 페이지 진입 +- 접수 등록 +- 목록 확인 +- 상세 확인 +- 승인자 승인 또는 반려 +- 운영 담당자 상태 갱신 +- 관리자 이슈 연결 확인 +- 사용자 상세 최종 상태 확인 +- 실패 케이스 점검 +- 권한 없는 사용자 진입 차단 +- `workspace` 미매핑 처리 +- 세션 만료 후 복귀 흐름 +- ABC 또는 내부 DB 저장 실패 시 안내 문구 +- 완료 기준 +- end-to-end 시연 체크리스트가 역할별로 1회 이상 통과되어야 함 + +## 5. 우선순위 기준 Task Backlog + +- P0 +- `workspace`, Project, Channel, Field, 권한 구조 확정 +- `support_tickets`, `abc_feedback_mappings`, `request_approvals` 최소 실DB 경로 검증 유지 +- 사용자 작성 -> 목록 -> 상세 기본 흐름 안정화 +- P1 +- 승인/반려, 이슈 연결, 댓글, 상태 반영 흐름 고도화 +- 동적 필드, 첨부, 알림, 운영 이력 보강 +- P2 +- 업무 시스템 연동, 알림 고도화, 확장 업무 테이블, 마이그레이션 추적 체계 보강 +- 2, 3은 최종 완료 판단을 위한 선행 조건으로 유지 + +## 6. 실무용 체크리스트 + +- 완료: FastAPI 앱 골격, `/api/health`, `/api/workspaces`, `/api/workspaces/{workspaceCode}/form-template`, `POST /api/workspaces/{workspaceCode}/tickets` 스텁, 1차 Alembic 마이그레이션 파일, `apps/web` 사용자 `/support` 라우트, `/ops`, `/admin/issues` 실제 페이지 이관 `(완료)` + +| 항목 | 상태 | 메모 | +| --- | --- | --- | +| ABC Web/API 로컬 실행 확인 | 완료 | `start-local.sh`, `check-local.sh` 실행 및 기본 포트/헬스체크 확인 완료 | +| MySQL 스키마 생성 | 진행중 | `apps/secretary-api` Alembic 1차 마이그레이션 파일 생성 완료. 실제 DB 적용 검증은 남아 있음 | +| MySQL 스키마 생성 | 부분 완료 | `baron_support` DB 생성, Alembic 1차/2차 적용, Docker bridge IP 기준 실DB 검증 완료. 로컬 공통 접속 방식 정리는 남아 있음 | +| `support_tickets` 테이블 생성 | 완료 | 실DB 생성 및 `description`, 상태 컬럼, `extra_fields` 포함 티켓 저장/조회 검증 완료 | +| `request_approvals` 테이블 생성 | 부분 완료 | 실DB 생성 확인 완료. 실제 승인 저장 흐름 검증은 다음 단계 | +| `abc_feedback_mappings` 테이블 생성 | 완료 | 실DB 생성 및 티켓 생성 시 매핑 레코드 저장 검증 완료 | +| ABC DB `feedbacks` 저장/조회 | 완료 | `apps/web` fallback 경로 기준으로 `userfeedback.feedbacks` 생성과 관리자 Feedback 화면 조회 검증 완료. 채널 필드 스키마에 맞춰 `contents`/`message` 본문만 저장되도록 정리 완료 | +| 사용자 작성/목록/상세 연결 | 완료 | `apps/web`에 `/support/[workspaceCode]/new|list|[ticketId]|index` 구현 및 작성->목록->상세 흐름 확인 | +| 승인 목록/운영 목록 연결 | 부분 완료 | `/ops`, `/admin/issues` 라우트와 승인/이슈 생성 스텁 흐름 구현. 실DB/실권한 연동은 남아 있음 | +| ABC 이슈 생성/연결 연동 | 부분 완료 | secretary-api 및 `/admin/issues` 경로에 내부 상태 전이/연결 스텁 구현. 실제 ABC API 연동은 남아 있음 | +| 처리 결과 사용자 노출 동기화 | 부분 완료 | 사용자 상세에 상태 배지 및 댓글 UI 노출. 실제 운영 결과 동기화는 남아 있음 | +| end-to-end 시연 검증 | 진행중 | 역할별 시나리오, 고정 데이터, 실패 케이스 체크리스트 초안 반영 | diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks_v2.md b/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks_v2.md new file mode 100644 index 0000000..973b75d --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_setup_tasks_v2.md @@ -0,0 +1,235 @@ +# BARON-SSO 연계 Task 정리 + +## 1. 문서 목적 + +본 문서는 현재 저장소 기준으로 이미 완료된 작업, 부분 완료 상태인 작업, 앞으로 진행해야 할 작업을 다시 정리한 실행 문서임. + +특히 다음 두 문서를 하나의 실행 기준으로 연결하는 목적을 가짐. + +- 사용자/운영 흐름 기준: `docs/architecture_secretary_sso_user_scenarios.md` +- 권한/테넌트/분기 기준: `docs/architecture_secretary_sso_role_access.md` + +이 문서에서는 기존 초안 중 현재 방향과 맞지 않는 항목은 정리하고, 실제 구현 상태와 다음 작업 순서를 우선으로 기록함. + +## 2. 현재 기준선 + +### 2.1 운영 구조 기준 + +- 인증 원본은 BARON-SSO 임. +- BARON-SSO는 인증과 현재 테넌트 문맥 확인까지만 담당함. +- 최종 권한과 관리자 여부는 내부 DB에서 판단함. +- 일반 사용자는 `/support/[workspaceCode]/**` 경로로 진입함. +- 관리자는 `/main/project/[projectId]/feedback?channelId=[channelId]` 중심 관리자 콘솔로 진입함. +- 초기 프로젝트 기준은 `EGBIM`, `TOVA`, `GAIA`, `KNGIL`, `Q&A_Platform` 임. +- 초기에는 프로젝트당 기본 채널 1개와 프로젝트 관리자 역할만 운영하고, 이후 필요 시 채널 관리자와 세분화 권한으로 확장함. +- 테스트 단계에서는 일반 사용자의 피드백 작성 페이지 프로젝트명과 workspace 식별자·표시명을 모두 `Q&A_Platform`으로 고정함. URL에서는 `Q%26A_Platform`으로 인코딩함. +- RP별 진입 버튼과 프로젝트/채널 분기는 전체 RP 목록과 매핑이 확정된 이후 별도 작업으로 진행함. + +### 2.2 상태 표기 기준 + +- `완료`: 현재 저장소와 로컬 검증 기준으로 동작 경로가 확인된 작업 +- `부분 완료`: 일부 구현 또는 연결은 되었지만, 최종 운영 기준으로는 비어 있는 작업 +- `대기`: 설계만 있고 아직 구현 또는 운영 반영이 시작되지 않은 작업 + +## 3. 완료된 작업 정리 + +### 3.1 로컬 실행 기반 + +| 항목 | 상태 | 정리 | +| ---------------------------------- | ---- | ------------------------------------------------------------------------------------- | +| 로컬 ABC/연관 서비스 실행 스크립트 | 완료 | `start-local.sh`, `check-local.sh` 기준 로컬 기동 경로가 정리되어 있음 | +| 기본 개발 저장소 구조 | 완료 | `apps/web`, `apps/api`, `apps/secretary-api`, `apps/e2e` 등 작업 단위가 분리되어 있음 | +| secretary-api 기본 앱 구조 | 완료 | FastAPI 엔트리포인트, health, tickets 라우트, Alembic 구조가 존재함 | + +### 3.2 사용자 지원 포털 + +| 항목 | 상태 | 정리 | +| ------------------------ | ---- | ---------------------------------------------------------------- | +| 사용자 작성 페이지 | 완료 | `/support/[workspaceCode]/new` 구현 완료 | +| 사용자 목록 페이지 | 완료 | `/support/[workspaceCode]/list` 구현 완료 | +| 사용자 상세 페이지 | 완료 | `/support/[workspaceCode]/[ticketId]` 구현 완료 | +| 기본 진입 리다이렉트 | 완료 | `/support/[workspaceCode]` 진입 시 목록 흐름 존재 | +| 폼 템플릿 조회 | 완료 | workspace 기반 작성 폼 템플릿 조회 API 연결 완료 | +| 작성/목록/상세 기본 흐름 | 완료 | 최소 1개 workspace 기준 작성 -> 목록 -> 상세 흐름 구현 완료 | +| 댓글 CRUD | 완료 | 사용자 상세와 관리자 상세 시트에서 댓글 생성/수정/삭제 흐름 존재 | +| 상세 상태 반영 | 완료 | 사용자 상세에서 내부 상태와 ABC 이슈 연결 상태를 함께 반영함 | + +### 3.3 secretary-api 및 내부 DB + +- 내부 권한 원본 분리 | 진행 | `baron_support.support_users`, `support_roles`, `support_role_assignments`를 추가하고, `user_workspace_access`는 유효 workspace 권한표로 사용 +- ABC 권한과 분리 | 진행 | Secretary의 로그인 후 분기와 지원 API는 ABC `users.type`, `roles`, `members`를 조회하지 않음 +- 기존 접근 데이터 백필 | 진행 | 기존 `user_workspace_access`를 내부 사용자와 workspace 역할 할당으로 이관 + +| 항목 | 상태 | 정리 | +| ------------------------- | ---- | ---------------------------------------------------------------------------------- | +| support ticket 기본 API | 완료 | workspaces, tickets, ticket detail, comments, approve, issue 관련 기본 라우트 존재 | +| 핵심 마이그레이션 1차/2차 | 완료 | `support_tickets`, 상태 컬럼, `extra_fields` 등 현재 UI 기준 컬럼 반영 완료 | +| 내부 티켓 저장 | 완료 | `support_tickets` 생성 흐름 구현 완료 | +| ABC 피드백 매핑 저장 | 완료 | 내부 티켓 생성 후 `abc_feedback_mappings` 저장 흐름 구현 완료 | +| 승인/이슈 상태 컬럼 | 완료 | `approval_status`, `sync_status`, `issue_link_status` 관리 구조 존재 | +| 코멘트 저장 구조 | 완료 | `ticket_comments` 및 관련 API 흐름 구현 완료 | +| 첨부 메타데이터 테이블 | 완료 | `attachments` 테이블과 ORM 모델 존재 | + +### 3.4 첨부파일 업로드 현재 완료 범위 + +| 항목 | 상태 | 정리 | +| ------------------------- | ---- | --------------------------------------------------------------------------------- | +| 작성 페이지 파일 선택 UI | 완료 | 작성 화면에서 다중 첨부 선택 가능 | +| multipart 프록시 처리 | 완료 | `apps/web` API route 에서 multipart 파싱 후 secretary-api 로 전달함 | +| secretary-api 업로드 수신 | 완료 | multipart 요청에서 `attachments` 수신 가능 | +| 로컬 파일 저장 | 완료 | 업로드 파일을 로컬 디렉터리에 저장하고 메타데이터를 `attachments` 테이블에 기록함 | +| 파일 크기 제한 | 완료 | 30MB 제한 설정 존재 | + +### 3.5 운영 보조 화면 + +| 항목 | 상태 | 정리 | +| -------------------------- | ---- | -------------------------------------------------------------- | +| `/ops` 페이지 | 완료 | 승인 대기/처리 흐름용 운영 보조 화면 존재 | +| `/admin/issues` 페이지 | 완료 | 이슈 연결 대상 확인용 운영 보조 화면 존재 | +| 관리자 상세 시트 댓글 연계 | 완료 | 관리자 피드백 상세 시트에서 support ticket 댓글 흐름 사용 가능 | + +## 4. 부분 완료 작업 정리 + +### 4.1 role_access 기준 운영 구조 반영 + +| 항목 | 상태 | 남은 내용 | +| ------------------------ | --------- | -------------------------------------------------------------------------------------- | +| 프로젝트 구조 문서화 | 부분 완료 | 새 기준 프로젝트 목록은 role_access 에 정리됐지만 실제 운영 seed/매핑 반영은 남아 있음 | +| 프로젝트/채널 실제 생성 | 대기 | `EGBIM`, `TOVA`, `GAIA`, `KNGIL`, `Q&A_Platform` 프로젝트와 동일명 기본 채널 생성 필요 | +| 관리자 접근 정책 | 대기 | 프로젝트/채널별 운영자 접근 범위와 내부 권한 테이블 반영 필요 | +| 문의 구분 기반 확장 전략 | 부분 완료 | 문서 초안은 있으나 실제 필드/라우팅/큐 분기 규칙은 미구현 | + +### 4.2 BARON-SSO 및 권한 분기 + +| 항목 | 상태 | 남은 내용 | +| ------------------------------------ | ---- | ------------------------------------------------------------------------------------- | +| BARON-SSO 로그인 연동 | 대기 | 실제 OIDC/OAuth 연동 구현 필요 | +| 세션의 SSO 식별자와 `tenant_id` 처리 | 대기 | 현재 테스트 사용자 상수 기반 흐름을 실제 세션 기반으로 전환해야 함 | +| 내부 사용자 매핑 | 대기 | SSO 식별자와 내부 `users` 또는 `auth_identities` 매핑 구조 미구현 | +| 사용자/관리자 페이지 분기 | 대기 | 로그인 후 내부 DB 권한 조회를 기준으로 `/support/...` 와 관리자 콘솔 자동 분기 미구현 | +| 프로젝트별 세부 권한 제한 | 대기 | 관리자 콘솔 진입 후 프로젝트별 재검증 로직 미구현 | +| 관리자 권한 설정 페이지 | 대기 | 관리자 콘솔 메뉴 내 권한 설정 페이지 추가 필요 | + +### 4.3 사용자 지원 포털 보강 + +| 항목 | 상태 | 남은 내용 | +| --------------------------- | --------- | -------------------------------------------------------------------- | +| 동적 필드 전체 사용 | 부분 완료 | 현재 title/description 중심 최소 렌더링만 사용 중 | +| 문의 구분 필드 반영 | 대기 | role_access 기준 문의 구분 저장 및 운영 큐 분기 연결 필요 | +| 비밀글 기능 | 부분 완료 | `support_tickets.is_secret`, 작성 폼, 작성자/관리자 조회 제한 연결 완료; E2E 및 마이그레이션 검증 필요 | +| 첨부파일 상세 조회/다운로드 | 대기 | 업로드 저장은 되지만 목록/상세 응답과 다운로드 경로는 없음 | +| 첨부파일 ABC 연동 | 대기 | 현재 ABC 생성 시 제목/본문만 전송하고 첨부는 내부 로컬 저장만 수행함 | +| 작성 완료 결과 표준화 | 부분 완료 | 현재 ticket 상태는 보이지만 운영 기준 완료 UX 는 추가 정리 필요 | + +### 4.4 운영/관리 기능 보강 + +| 항목 | 상태 | 남은 내용 | +| ------------------------------ | --------- | --------------------------------------------------------------------------- | +| 승인 이력 정교화 | 부분 완료 | 승인 상태 전이와 기본 API 는 있으나 실제 운영 권한/사유/이력 정책 보강 필요 | +| 이슈 생성/연결 운영 흐름 | 부분 완료 | 상태 동기화와 보조 화면은 있으나 실제 운영 정책/권한 제어는 추가 필요 | +| 담당자 배정/처리 메모 | 대기 | 전담 운영 테이블/화면/이력 흐름 미구현 | +| 관리자 콘솔 프로젝트 단위 제한 | 대기 | role_access 기준 프로젝트별 접근 제한 미구현 | +| 관리자 권한 설정 UI | 대기 | 콘솔 메뉴와 설정 화면에서 프로젝트 관리자 부여/해제 기능 필요 | + +### 4.5 데이터 및 운영 자동화 + +| 항목 | 상태 | 남은 내용 | +| ----------------- | --------- | ------------------------------------------------------------------------------- | +| seed 데이터 정리 | 부분 완료 | 테스트 흐름은 있으나 새 프로젝트 기준 seed 재정리 필요 | +| 공통 권한 테이블 | 대기 | 우선 `user + project + role` 조합 저장 구조 구체화 필요 | +| 알림/후속 연계 | 대기 | 승인 완료, 처리 완료, 설정 누락 알림 등 운영 이벤트 미구현 | +| E2E 시나리오 고정 | 부분 완료 | 화면 시연은 가능하나 role_access 기준 사용자/관리자 분기 시나리오 정리는 부족함 | + +## 5. 앞으로 해야 할 작업 + +### 5.1 P0: role_access 기준 운영 구조 확정 + +- BARON-SSO에서 받을 사용자 식별 claim 확정 +- 내부 사용자 매핑 키 확정 +- 프로젝트 `EGBIM`, `TOVA`, `GAIA`, `KNGIL`, `Q&A_Platform` 실제 생성 +- 각 프로젝트 기본 채널 1개 생성 +- 사용자 Q&A 이동 URL과 `workspaceCode` 매핑 표 확정 +- 테스트 단계에서는 모든 일반 사용자 작성 화면의 프로젝트명과 workspace 표시명을 `Q&A_Platform`으로 고정 +- 프로젝트별 관리자 접근 정책 확정 + +### 5.2 P0: 로그인 후 분기와 권한 처리 구현 + +- BARON-SSO 로그인 연동 구현 +- 세션에서 SSO 식별자와 `tenant_id` 를 읽는 공통 계층 추가 +- 내부 사용자 및 프로젝트 관리자 권한 조회 계층 추가 +- 일반 사용자 -> `/support/[workspaceCode]/new` 이동 구현 +- 관리자 -> 관리자 콘솔 기본 진입 경로 이동 구현 +- 관리자 콘솔 진입 후 프로젝트별 재검증 구현 +- 권한 설정 페이지를 관리자 콘솔 메뉴에 추가 + +### 5.3 P1: 사용자 포털 기능 마감 + +- 문의 구분 필드 추가 및 저장 +- 비밀글 필드 추가 및 저장 `(부분 완료 - support_tickets.is_secret 및 사용자 폼 연결)` +- 비밀글 조회 권한 및 마스킹 정책 구현 `(작성자/관리자 제한 적용, E2E 검증 필요)` +- `workspace`별 동적 필드 전체 렌더링 정리 +- 첨부파일 응답 스키마 추가 +- 사용자 상세 첨부 목록 및 다운로드 구현 +- 필요 시 첨부 ABC 저장 전략 확정 후 브릿지 구현 +- 업로드 실패/부분 저장 실패 시 사용자 안내 문구 표준화 + +### 5.4 P1: 운영/관리 기능 마감 + +- 승인/반려 사유와 승인 이력 화면 정리 +- 담당자 배정, 처리 메모, 상태 변경 이력 구현 +- 이슈 생성 후 사용자 상세 상태 반영 규칙 정리 +- 관리자 콘솔에서 프로젝트/문의구분 기반 큐 분리 +- 운영 보조 화면 `/ops`, `/admin/issues` 와 실제 관리자 콘솔 역할 분담 정리 +- 관리자 권한 설정 화면에서 프로젝트 관리자 관리 기능 구현 + +### 5.5 P2: 운영 안정화 및 검증 + +- role_access 기준 테스트 계정 3종 이상 준비 +- 일반 사용자/담당자/시스템 관리자 시나리오별 E2E 체크리스트 작성 +- 프로젝트 미매핑, 권한 부족, 세션 만료 예외 처리 검증 +- 비밀글 작성자, 관리자, 권한 없는 사용자 시나리오 검증 +- 알림 및 운영 설정 누락 감지 체계 추가 +- 문서 간 용어 통일: tenant, workspace, project, channel, 문의 구분 + +## 6. 바로 실행할 다음 작업 제안 + +### 6.1 1차 묶음 + +- 테스트용 기본 프로젝트명 `Q&A_Platform` 고정 흐름 검증 +- RP별 진입 버튼 -> `workspaceCode` -> ABC `project_id/channel_id` 매핑표 확정 +- SSO 식별자 -> 내부 사용자 매핑 규칙 확정 +- 로그인 후 사용자/관리자 분기 미들웨어 또는 라우터 초안 작성 + +### 6.2 2차 묶음 + +- 비밀글 저장/조회 제한 API 추가 `(부분 완료 - 생성/응답/작성자·관리자 조회 제한)` +- 첨부파일 조회/다운로드 API 추가 +- 사용자 상세 첨부 표시 추가 +- 문의 구분 필드 저장 및 관리자 큐 표시 초안 추가 + +### 6.3 3차 묶음 + +- 프로젝트별 관리자 접근 제어 테이블 설계 +- 운영 보조 화면과 실제 관리자 콘솔 권한 경계 정리 +- role_access 기준 E2E 시나리오 문서화 + +## 7. 이번 정리에서 제거한 구버전 가정 + +- `INTRANET_SUPPORT`, `SOFTWARE_QA` 중심 Project 초안은 현재 우선 기준에서 제외함. +- 기존 task 문서에 있던 인트라넷 신청형 업무 중심 Channel 목록은 role_access 기준 Q&A 프로젝트 구조가 확정될 때까지 보조 아이디어로만 취급함. +- 테스트 상수 사용자 기준 흐름은 임시 검증 수단으로 유지하되, 운영 기준 완료 항목으로 보지 않음. + +## 8. 최종 요약 + +- 현재 구현은 사용자 지원 포털, 내부 티켓 저장, 댓글, 일부 운영 보조 화면까지는 갖춰져 있음. +- 새 기준선은 BARON-SSO 인증 연동, 내부 DB 권한 모델, 프로젝트 관리자 중심 운영 구조, 관리자 권한 설정 페이지, 비밀글 기능임. +- 지금 가장 큰 공백은 SSO 실연동, 로그인 후 내부 DB 권한 분기, 프로젝트별 접근 제어, 비밀글, 첨부 조회/다운로드, 문의 구분 기반 운영 큐 분리임. +- 이후 작업은 SSO에서 신원만 받고, 권한과 화면 제어는 내부 DB 기준으로 반영하는 순서로 진행해야 함. + +## 15. 테스트 단계 프로젝트 표시 및 RP 분기 보류 기준 + +- 현재 테스트 단계의 일반 사용자 피드백 작성 페이지는 프로젝트명과 workspace 식별자·표시명을 모두 `Q&A_Platform`으로 고정 표시함. URL에서는 `Q%26A_Platform`으로 인코딩함. +- 현재 로그인 후 기본 진입은 테스트용 단일 작성 흐름을 검증하기 위한 임시 동작으로 취급함. +- RP별 진입 버튼은 각 RP 식별자 또는 `workspaceCode`를 전달하고, 서버에서 ABC `project_id`와 `channel_id`로 매핑하는 구조를 최종 기준으로 삼음. +- 전체 RP 목록, 프로젝트명, 기본 채널, API Key, 권한 범위가 확정되기 전까지 RP별 작성 페이지 자동 분기는 구현하지 않음. +- RP 분기 구현 시 URL의 프로젝트명만 신뢰하지 않고, 서버 측 매핑과 사용자 workspace 접근 권한을 함께 검증함. diff --git a/docs/관리페이지 md 파일/architecture_secretary_sso_user_scenarios.md b/docs/관리페이지 md 파일/architecture_secretary_sso_user_scenarios.md new file mode 100644 index 0000000..02977e5 --- /dev/null +++ b/docs/관리페이지 md 파일/architecture_secretary_sso_user_scenarios.md @@ -0,0 +1,502 @@ +# 사내 지원 플랫폼 사용자별 사용 시나리오 + +## 1. 문서 목적 + +본 문서는 [architecture_secretary_sso_components_v2.md](./architecture_secretary_sso_components_v2.md)에서 정의한 통합 아키텍처를 바탕으로, 사용자 유형별 실제 사용 시나리오를 정리한 문서임. + +핵심 목적은 다음과 같음. + +- 어떤 사용자가 어떤 경로로 진입하는지 명확히 구분함. +- BARON-SSO, 우리 시스템, ABC User Feedback가 각각 어느 시점에 개입하는지 흐름으로 표현함. +- 조회 결과가 보이는 지점과 처리 결과가 저장되는 지점을 시나리오별로 시각화함. + +## 2. 사용자 유형 정의 + +| 사용자 유형 | 주요 진입 경로 | 주요 목적 | 로그인 후 주 분기 화면 | +| --- | --- | --- | --- | +| 인트라넷 일반 사용자 | 인트라넷 포털 또는 공통 업무 진입점 | 물품 신청, 도서 신청, 차량 신청, 사내 문의 등록 | 사용자 피드백 작성 페이지, 내 피드백 목록/상세, ABC 조회 화면 | +| S/W 프로그램 사용자 | 각 S/W 프로그램 메뉴 또는 공통 업무 진입점 | 앱 전용 Q&A 등록, 장애/문의 접수 | 사용자 피드백 작성 페이지, 내 문의 목록/상세, ABC 조회 화면 | +| 승인자 | 인트라넷 또는 운영 포털의 공통 진입점 | 승인 대기 건 검토, 승인/반려 처리 | 승인 대상 목록, 피드백 상세, ABC 관리자 UI | +| 운영 담당자 | 운영 메뉴 또는 공통 진입점 | 전체 티켓 관리, 상태 변경, 후속 조치, 공지/알림 | 운영 목록/상세, 이슈 관리, ABC 관리자 UI | + +## 3. 사용 기술 스택 + +| 영역 | 사용 기술 | 역할 | +| --- | --- | --- | +| 인증/사용자 식별 | BARON-SSO, OAuth 2.0, OIDC | 사용자 로그인, `user_id`, `tenant_id`, 토큰 발급 | +| 사용자/운영 화면 | 사용자 피드백 작성 페이지, ABC User Feedback Web Frontend | 사용자 입력, 목록 조회, 상세 확인, 이슈 관리, 관리자 설정 | +| 제어 백엔드 | FastAPI | `workspace` 매핑, 권한 분기, 승인 워크플로우, 외부 연동 orchestration | +| 제어 데이터 저장소 | 자체 DB (예: MySQL) | `support_tickets`, `request_approvals`, 매핑, 운영 이력 저장 | +| 원본 피드백 저장소 | ABC User Feedback | 피드백 원문, 첨부, 채널, 필드, 이슈 관리 | +| 외부 알림 연동 | Naver Works API, SMS Gateway | 승인/반려/처리 결과 알림 | +| 업무 시스템 연동 | 자산 API, 차량 API, 조직도 API 등 | 후속 처리 자동화 및 업무 데이터 조회 | + +기술 스택의 책임 분리는 다음과 같음. + +- BARON-SSO는 인증과 조직 식별의 기준 시스템임. +- 사용자와 승인자, 운영 담당자는 동일한 인증 상태에서 접속하고, 로그인 후에는 역할과 업무 컨텍스트에 따라 화면이 분기됨. +- 사용자 피드백 작성 페이지는 일반 사용자의 직접 입력 경험을 담당함. +- ABC User Feedback Web Frontend는 원문 조회와 운영 담당자용 관리 화면을 담당함. +- FastAPI는 ABC와 자체 DB 사이에서 정책과 절차를 제어하는 핵심 백엔드임. +- ABC User Feedback는 게시글 원본과 운영용 채널/이슈 관리 기능을 담당함. + +## 4. 사용자별 시나리오 맵 + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U1[인트라넷 일반 사용자] + U2[S/W 프로그램 사용자] + U3[승인자] + U4[운영 담당자] + + ENTRY[공통 업무 진입점\n포털 / 앱 / 운영 메뉴]:::result + SSO[BARON-SSO]:::external + G1[우리 시스템\n세션 확인 / 역할 판정 / workspace 매핑]:::control + UI1[사용자 화면 분기 +작성 / 내 목록 / 내 상세] +:::result + UI2[운영 화면 분기 +승인 목록 / 운영 목록 / 상세] +:::result + UI3[ABC User Feedback 관리자 UI]:::result + APP[우리 시스템\n권한/워크플로우/매핑]:::control + ABC[ABC User Feedback\n원본 저장소]:::result + + U1 -->|공통 진입| ENTRY + U2 -->|공통 진입| ENTRY + U3 -->|공통 진입| ENTRY + U4 -->|공통 진입| ENTRY + + ENTRY -->|로그인 세션 확인 또는 SSO 요청| SSO + SSO -->|user_id, tenant_id, role context 반환| G1 + + G1 -->|일반 사용자 + 신청/문의 컨텍스트| UI1 + G1 -->|승인자/운영자 + 운영 컨텍스트| UI2 + UI2 -->|필요 시 ABC 관리자 기능 진입| UI3 + + UI1 -->|등록/조회 요청| APP + UI2 -->|승인/운영 요청| APP + UI3 -->|원문/이슈 관리 요청| APP + APP -->|원본 데이터 저장/조회| ABC + APP -->|상태/승인/권한 기록| APP +``` + +## 5. 시나리오 1: 인트라넷 일반 사용자의 신청 등록 + +### 5.1 시나리오 설명 + +인트라넷 일반 사용자는 물품 신청, 도서 신청, 차량 신청, 사내 문의와 같은 업무를 등록하는 주체임. 이 사용자는 우리 시스템이 직접 구현한 사용자 피드백 작성 페이지에서 신청 내용을 입력하고, 우리 시스템은 그 뒤의 승인 정책과 처리 상태를 제어하며 원문은 ABC에 저장함. + +### 5.2 주요 단계 + +1. 사용자가 인트라넷 포털에서 특정 지원 서비스를 선택함. +2. 사용자 피드백 작성 페이지가 BARON-SSO 인증을 수행하고 `user_id`, `tenant_id`를 확보함. +3. 우리 시스템이 진입한 서비스 코드를 `workspace`로 매핑하고, 해당 사용자를 적절한 ABC 프로젝트/채널로 연결함. +4. 사용자는 우리 시스템이 직접 구현한 입력 화면에서 신청 내용을 작성함. +5. 사용자가 신청서를 제출하면 ABC가 원본 피드백 데이터를 저장함. +6. 우리 시스템은 생성된 `feedback_id`를 내부 티켓과 매핑하고 상태, 승인 필요 여부, 운영 메타데이터를 저장함. +7. 사용자와 담당자는 ABC UI에서 원문을 보고, 우리 시스템은 상태와 승인 결과를 동기화함. + +### 5.3 Flow + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U[인트라넷 일반 사용자] + P[인트라넷 포털] + SSO[BARON-SSO]:::external + UI[사용자 피드백 작성 페이지 +우리 시스템]:::result + M1[우리 시스템\nworkspace 매핑 / 접근 제어]:::control + M3[우리 시스템\n티켓 메타데이터 저장]:::control + A1[ABC Feedback 저장 처리]:::control + A2[ABC 원본 데이터 저장소]:::result + D1[우리 DB\nsupport_tickets / approvals / mappings]:::result + R1[ABC 접수 결과 화면\n내 신청 내역]:::result + + U -->|지원 서비스 선택| P + P -->|신청 화면 호출| UI + UI -->|SSO 인증 요청| SSO + SSO -->|user_id, tenant_id 반환| UI + UI -->|서비스 코드 전달| M1 + M1 -->|workspace 기반 프로젝트/채널 연결| UI + UI -->|입력 화면 작성 및 제출| A1 + A1 -->|본문/필드값 저장| A2 + A2 -->|feedback_id 생성 이벤트 전달| M3 + M3 -->|상태/승인여부/매핑 기록| D1 + A2 -->|원문 조회 제공| R1 + A2 -->|원본 접수 데이터 확보| R1 +``` + +## 6. 시나리오 2: S/W 프로그램 사용자의 Q&A 등록 + +### 6.1 시나리오 설명 + +S/W 프로그램 사용자는 특정 앱 내부의 도움말 또는 지원 메뉴를 통해 진입하며, 본인이 사용 중인 앱에 대응하는 전용 사용자 피드백 작성 페이지로 연결됨. 우리 시스템은 이를 적절한 ABC Q&A 채널에 매핑하여 원문을 저장함. + +### 6.2 주요 단계 + +1. 사용자가 특정 S/W 프로그램 내 지원 메뉴를 클릭함. +2. 앱이 `app_id` 또는 서비스 식별자를 포함한 상태로 사용자 피드백 작성 페이지 진입 URL을 호출함. +3. BARON-SSO 인증 후 우리 시스템이 해당 식별자를 `workspace`와 ABC 채널로 매핑함. +4. 사용자는 우리 시스템의 사용자 피드백 작성 페이지에서 앱 전용 문의 폼을 작성함. +5. 사용자가 문의를 등록하면 ABC에 원본 피드백이 저장됨. +6. 우리 시스템은 내부 티켓과 앱-채널 매핑 정보를 함께 저장함. +7. 사용자는 필요 시 ABC 조회 화면에서 본인 문의 원문을 확인하고, 우리 시스템은 처리 상태를 별도 메타데이터로 관리함. + +### 6.3 Flow + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U[S/W 프로그램 사용자] + APP0[각 S/W 프로그램] + SSO[BARON-SSO]:::external + UI[사용자 피드백 작성 페이지 +우리 시스템]:::result + B1[우리 시스템\napp_id -> workspace 매핑]:::control + B2[우리 시스템\n권한/채널 판정]:::control + A1[ABC 채널/필드 구성]:::result + A2[ABC Feedback 저장 처리]:::control + A3[ABC 원본 Q&A 저장소]:::result + D1[우리 DB\nworkspace_channel_mappings\nsupport_tickets]:::result + R1[ABC 문의 등록 결과\n내 문의 목록]:::result + + U -->|지원 메뉴 클릭| APP0 + APP0 -->|app_id 포함 화면 호출| UI + UI -->|SSO 인증 요청| SSO + SSO -->|사용자 식별 정보 반환| UI + UI -->|app_id 전달| B1 + B1 -->|workspace 및 채널 결정| B2 + B2 -->|작성 페이지용 채널 매핑 전달| UI + B2 -->|채널 필드 정의 참조| A1 + UI -->|문의 등록 제출| A2 + A2 -->|Q&A 원본 저장| A3 + B2 -->|내부 티켓 및 매핑 기록| D1 + A3 -->|문의 원본 표시| R1 + D1 -->|처리 상태 표시| R1 +``` + +## 7. 시나리오 3: 승인자의 승인/반려 처리 + +### 7.1 시나리오 설명 + +승인자는 일반 사용자와 동일한 로그인 상태에서 접속한 뒤, 역할과 업무 컨텍스트에 따라 승인 화면으로 분기되어 원문과 이슈를 확인하면서 우리 시스템이 제어하는 승인 정책에 따라 승인 여부를 결정하는 역할임. 승인 결과는 상태 변화와 알림 발송으로 이어짐. + +### 7.2 주요 단계 + +1. 승인자가 공통 업무 진입점에서 승인 업무로 접속함. +2. 로그인 세션 확인 또는 BARON-SSO 인증 후 우리 시스템이 승인 권한과 ABC 프로젝트 접근 권한을 검증 및 동기화함. +3. 승인자는 역할에 따라 분기된 운영 화면 또는 ABC 관리자 UI에서 승인 대상 피드백과 연결 이슈를 조회함. +4. 승인자가 승인 또는 반려 판단을 수행하면 우리 시스템이 해당 결과를 내부 승인 로직에 반영함. +5. 우리 시스템이 `request_approvals`와 티켓 상태를 갱신함. +6. 필요 시 메신저 또는 SMS 알림을 발송함. +7. 사용자와 운영자는 분기된 조회 화면과 ABC UI, 내부 상태 동기화 결과를 기준으로 처리 결과를 확인함. + +### 7.3 Flow + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U[승인자] + UI[승인 화면 분기\n우리 시스템 운영 화면 / ABC 관리자 UI]:::result + SSO[BARON-SSO]:::external + C1[우리 시스템\n승인 권한 판정 / 멤버 동기화]:::control + C2[ABC 피드백 / 이슈 조회]:::control + C3[우리 시스템\n승인/반려 처리]:::control + D1[우리 DB\nrequest_approvals / support_tickets]:::result + X1[Naver Works / SMS]:::external + R1[ABC 결과 화면\n처리 상태]:::result + + U -->|공통 진입 후 승인 업무 선택| UI + UI -->|세션 확인 또는 SSO 인증 요청| SSO + SSO -->|user_id, role context 반환| UI + UI -->|승인 대상 조회 요청| C1 + C1 -->|권한 확인 후 피드백/이슈 조회| C2 + C2 -->|승인 대상 데이터 반환| UI + UI -->|승인/반려 판단 수행| C3 + C3 -->|승인 이력 및 상태 갱신| D1 + C3 -->|결과 알림 발송 요청| X1 + D1 -->|처리 결과 반영| R1 +``` + +## 8. 시나리오 4: 운영 담당자의 후속 조치 및 이슈 관리 + +### 8.1 시나리오 설명 + +운영 담당자는 일반 사용자와 동일한 로그인 상태에서 접속한 뒤, 역할과 업무 컨텍스트에 따라 운영 화면으로 분기되어 접수 건을 실제 처리 단계로 연결하는 역할을 담당함. 예를 들어 자산 배정, 차량 배차, 원격지원, Q&A 이슈화, 상태 종료 처리 등을 수행함. + +### 8.2 주요 단계 + +1. 운영 담당자가 공통 업무 진입점에서 운영 업무로 접속함. +2. 로그인 세션 확인 또는 BARON-SSO 인증 후 우리 시스템이 운영 권한과 조회 가능한 `workspace` 범위를 판정함. +3. 분기된 운영 화면 또는 ABC 관리자 UI에서 전체 피드백 또는 특정 `workspace`에 대응되는 채널을 조회함. +4. 우리 시스템이 상태, 우선순위, 요청 유형, 담당자 기준 메타데이터를 ABC 데이터와 동기화함. +5. 운영 담당자가 특정 피드백을 열어 원본 데이터와 승인 이력을 함께 확인함. +6. 필요 시 ABC 원문에 대응되는 이슈를 생성하거나 연결함. +7. 우리 시스템이 자산/차량/원격지원/알림 등의 확장 프로세스를 수행함. +8. 처리 완료 후 내부 상태를 종료하거나 추가 조치 필요 상태로 갱신하고, 필요한 결과를 ABC에 반영함. +9. 사용자와 운영자는 분기된 조회 화면과 ABC UI를 기준으로 최신 원문과 처리 상태를 확인함. + +### 8.3 Flow + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U[운영 담당자] + UI[운영 화면 분기\n우리 시스템 운영 화면 / ABC 관리자 UI]:::result + C1[우리 시스템\n목록 필터/상태 조회]:::control + C2[우리 시스템\n후속 조치 엔진]:::control + C3[우리 시스템\n이슈/알림/자산 연계]:::control + D1[우리 DB\n티켓/승인/운영 이력]:::result + A1[ABC 원본 데이터]:::result + A2[ABC 이슈/피드백 연결]:::control + X1[자산/차량/외부 API]:::external + R1[ABC 운영 결과 화면\n최신 상태]:::result + + U -->|공통 진입 후 운영 업무 선택| UI + UI -->|조건별 검색 요청| C1 + C1 -->|티켓/상태/담당자 조회| D1 + D1 -->|목록 및 상세 데이터 반환| UI + UI -->|원문 확인 요청| A1 + UI -->|후속 조치 실행| C2 + C2 -->|이슈 연결 또는 상태 갱신| A2 + C2 -->|운영 이력 저장| D1 + C3 -->|자산/차량/알림 API 호출| X1 + D1 -->|최신 결과 집계| R1 + A2 -->|원본 연계 상태 반영| R1 +``` + +## 9. 권한별 핵심 차이 + +| 구분 | 일반 사용자 | S/W 사용자 | 승인자 | 운영 담당자 | +| --- | --- | --- | --- | --- | +| 인증 | BARON-SSO | BARON-SSO | BARON-SSO | BARON-SSO | +| 진입 기준 | 서비스 메뉴 | 앱 메뉴 | 승인 업무 메뉴 | 운영 메뉴 | +| 주 작업 | 신청 등록/조회 | 문의 등록/조회 | 승인/반려 | 상태 관리/후속 조치 | +| ABC 직접 사용 여부 | 부분 사용 | 부분 사용 | 직접 사용 | 직접 사용 | +| 우리 시스템 의존도 | 높음 | 높음 | 높음 | 높음 | +| 핵심 저장 위치 | ABC + 우리 DB | ABC + 우리 DB | ABC + 우리 DB | ABC + 우리 DB | + +## 10. 문서 활용 가이드 + +- 화면 설계 시에는 일반 사용자용 작성 화면은 우리 시스템에서 직접 구현하고, 운영/관리 화면은 ABC 기본 UI를 우선 활용함. +- API 설계 시에는 어떤 단계가 우리 시스템 API인지, 어떤 단계가 ABC 연동인지 분리해서 정의함. +- 권한 설계 시에는 `workspace`, `role`, `approval permission`, `operator permission`을 별도 축으로 설계함. +- 운영 정책 수립 시에는 승인자와 운영 담당자의 역할이 섞이지 않도록 본 문서의 시나리오를 기준으로 책임 범위를 정리함. + +## 11. 시나리오 기준 ABC User Feedback 활용 기능 및 API + +### 11.1 활용 원칙 + +- 최종 사용자 인증은 BARON-SSO를 기준으로 하고, ABC의 사용자 인증 체계는 운영용 또는 관리용 보조 수단으로만 사용함. +- 일반 사용자는 우리 시스템의 사용자 피드백 작성 페이지를 사용하고, 운영 담당자는 ABC User Feedback UI를 직접 사용함. +- 우리 시스템은 사용자 입력 화면, ABC UI 진입 제어, 권한 동기화, 상태/승인 메타데이터 관리에 집중함. +- 따라서 ABC API는 크게 `프로젝트/채널 사전 구성`, `원본 피드백 저장/조회`, `이슈 관리`, `멤버/역할 동기화`, `운영 자동화` 용도로 사용함. + +### 11.2 시나리오별 ABC 활용 기능 + +| 시나리오 | ABC에서 활용할 기능 | 실제 활용 API | 비고 | +| --- | --- | --- | --- | +| 인트라넷 일반 사용자 신청 등록 | 신청 원문 저장 | `POST /api/projects/:projectId/channels/:channelId/feedbacks` | 우리 작성 페이지에서 API 호출로 ABC 원문 저장 | +| 인트라넷 일반 사용자 신청 등록 | 이미지 포함 신청 저장 | `POST /api/projects/:projectId/channels/:channelId/feedbacks-with-images` | 첨부 업로드 자동화가 필요한 경우 사용 | +| S/W 프로그램 사용자 Q&A 등록 | 앱 전용 문의 원문 저장 | `POST /api/projects/:projectId/channels/:channelId/feedbacks` | 우리 작성 페이지에서 `workspace`와 채널 매핑 후 API 호출 | +| 승인자 승인/반려 | 원문 참조용 피드백 목록 조회 | `POST /api/admin/projects/:projectId/channels/:channelId/feedbacks/search` | ABC 관리자 UI와 병행 사용 | +| 운영 담당자 후속 조치 | 이슈 생성 | `POST /api/admin/projects/:projectId/issues` | 문의를 이슈로 승격할 때 사용 | +| 운영 담당자 후속 조치 | 피드백-이슈 연결 | `POST /api/admin/projects/:projectId/channels/:channelId/feedbacks/:feedbackId/issue/:issueId` | 문의와 처리 이슈 연결 | +| 운영 담당자 후속 조치 | 이슈 목록 조회 | `POST /api/admin/projects/:projectId/issues/search` | 현황판, 대시보드 구성에 사용 | +| 운영 담당자 후속 조치 | 이슈 상세 조회 | `GET /api/admin/projects/:projectId/issues/:issueId` | 연결된 피드백 수, 상태 확인 | +| 운영 담당자 후속 조치 | 피드백 수정 | `PUT /api/admin/projects/:projectId/channels/:channelId/feedbacks/:feedbackId` | 원문 보정 또는 메타 필드 갱신 | +| 운영 담당자 후속 조치 | 피드백 내보내기 | `POST /api/admin/projects/:projectId/channels/:channelId/feedbacks/export` | 운영 보고용 다운로드 | +| 운영 환경 준비 | 프로젝트 생성/조회 | `POST /api/admin/projects`, `GET /api/admin/projects` | 서비스 단위 프로젝트 생성 | +| 운영 환경 준비 | 채널 생성/조회 | `POST /api/admin/projects/:projectId/channels/`, `GET /api/admin/projects/:projectId/channels/` | 서비스별 접수 채널 구성 | +| 운영 환경 준비 | 채널 필드 관리 | `PUT /api/admin/projects/:projectId/channels/:channelId/fields` | 신청서 항목과 채널 필드 정합 | +| 운영 권한 구성 | 프로젝트 멤버 관리 | `POST /api/admin/projects/:projectId/members`, `POST /api/admin/projects/:projectId/members/search` | 운영자/담당자 접근 권한 부여 | +| 운영 권한 구성 | 역할 관리 | `GET /api/admin/projects/:projectId/roles/`, `POST /api/admin/projects/:projectId/roles/` | 프로젝트별 역할 정의 | + +일반 사용자와 운영 담당자의 일상 사용 흐름은 ABC UI를 우선 사용하고, 위 API는 다음 목적에서 활용함. + +- 초기 프로젝트/채널/역할 셋업 자동화 +- BARON-SSO 사용자와 ABC 멤버 구조 동기화 +- 내부 상태값과 ABC 피드백/이슈 연계 자동화 +- 대량 등록, 이관, 배치 처리, 외부 시스템 연동 + +### 11.3 ABC에서 주로 재사용할 기능 묶음 + +| 기능 묶음 | 재사용 목적 | 설명 | +| --- | --- | --- | +| Feedback 저장 기능 | 문의/신청 원문 저장 | 제목, 본문, 필드값, 이미지 등 원본 데이터 보존 | +| Channel/Field 관리 기능 | 서비스별 입력 스키마 구성 | 채널별 신청서 항목과 커스텀 필드 유지 | +| Issue 관리 기능 | 운영 후속 조치 기록 | 피드백을 운영 이슈와 연결하고 상태 추적 | +| Project/Member/Role 관리 기능 | 운영 구조 설정 | 프로젝트, 채널, 담당자, 권한 체계 운영 | +| Export 기능 | 운영 보고/분석 | 피드백 데이터 다운로드 및 외부 분석 활용 | + +## 12. 우리가 직접 구현해야 하는 부분 + +### 12.1 구현 원칙 + +- ABC는 원본 데이터와 관리자 기능을 담당하고, 실제 서비스 경험은 우리 시스템이 완성함. +- 일반 사용자가 사용하는 사용자 피드백 작성 페이지는 우리 시스템에서 직접 구현하고, 운영/관리 기능은 ABC UI를 최대한 활용함. +- 따라서 인증, 권한, 워크플로우, 업무 상태, 외부 시스템 연계뿐 아니라 일반 사용자 입력 화면도 직접 구현 범위에 포함됨. + +### 12.2 직접 구현 범위 요약 + +| 구현 영역 | 우리가 직접 구현할 내용 | ABC만으로 부족한 이유 | +| --- | --- | --- | +| SSO 연동 | BARON-SSO 로그인, 토큰 검증, `user_id`, `tenant_id` 세션 처리 | ABC 인증은 우리 조직의 통합 인증 기준이 아님 | +| 진입 경로 해석 | `app_id`, `service_type_id`, 메뉴 코드 등을 `workspace`로 매핑 | ABC는 외부 서비스 진입 맥락을 이해하지 못함 | +| 사용자 피드백 작성 화면 | 일반 사용자용 입력 폼, 유효성 검사, 제출 완료 UX, ABC 저장 API 호출 | ABC 기본 화면만으로는 서비스별 사용자 경험과 진입 컨텍스트를 충분히 통제하기 어려움 | +| ABC 진입 제어 | 사용자를 올바른 프로젝트/채널/화면으로 연결하는 리다이렉트 및 딥링크 로직 | ABC는 외부 포털 메뉴 체계와 직접 연결되지 않음 | +| 채널/필드 표준화 | 서비스별 필드 템플릿, 채널 생성 규칙, 운영 정책 자동화 | ABC만으로는 사내 표준 신청서 체계를 일관되게 강제하기 어려움 | +| 내부 티켓 모델 | `support_tickets` 기반 상태, 우선순위, 업무 유형, 담당자 관리 | ABC 피드백 원문만으로 운영 제어 메타데이터를 관리하기 부족함 | +| 승인 워크플로우 | `request_approvals`, 승인선, 승인/반려 이력, 다단계 결재 | 사내 결재 정책은 ABC 기본 기능 범위를 넘음 | +| 권한 동기화 | 사용자별 조회 범위, 승인 권한, 운영 권한, 워크스페이스별 접근 제어를 ABC 멤버/역할과 연동 | 프로젝트/멤버 권한만으로 세밀한 업무 분기 어려움 | +| 원본-내부 매핑 | ABC `feedback_id`와 내부 티켓 ID, 기존 시스템 ID 매핑 | 이관 및 통합 운영을 위한 별도 식별자 체계 필요 | +| 외부 연동 | 자산, 차량, 조직도, 메신저, SMS, 기타 사내 API 연계 | ABC는 사내 업무 시스템 orchestration 역할이 아님 | +| 알림 정책 | 상태 변경별 알림 템플릿, 채널 선택, 재발송 규칙 | 사내 정책 기반 알림 제어가 필요함 | +| 집계/리포팅 | 업무 유형별 KPI, 승인 지연, 처리 SLA, 부서별 통계 | ABC 통계는 일반 피드백 관점이라 업무형 리포트에 한계가 있음 | +| 마이그레이션 도구 | 기존 게시글, 첨부, 댓글, 분류 정보 이관 및 검증 | 기존 시스템 식별자와의 추적 관리가 필요함 | + +### 12.3 시나리오별 구현 책임 정리 + +| 시나리오 | ABC가 담당하는 부분 | 우리가 직접 구현하는 부분 | +| --- | --- | --- | +| 인트라넷 일반 사용자 신청 등록 | 피드백 원문 저장, 첨부 저장 | SSO 로그인, 사용자 피드백 작성 페이지 구현, 서비스 진입 제어, 채널 연결, 신청 상태 저장, 승인선 생성 | +| S/W 프로그램 사용자 Q&A 등록 | 앱 전용 채널에 문의 원문 저장, 목록/상세 UI 제공 | 사용자 피드백 작성 페이지 구현, 앱 식별자 해석, `workspace` 매핑, 사용자 권한 판정, ABC 진입 URL 제어, 상태 메타데이터 관리 | +| 승인자 승인/반려 | 피드백/이슈 조회 UI 제공 | 승인 규칙, 승인 이력, 결과 알림, 상태 전이, ABC 멤버 권한 동기화 | +| 운영 담당자 후속 조치 | 이슈 생성, 피드백-이슈 연결, 원문 검색/수정 UI 제공 | 담당자 배정, 자산/차량 후속 처리, SLA 추적, 내부 상태 관리, 외부 API 실행 | +| 운영 환경 준비 | 프로젝트/채널/필드/멤버/역할 관리 API와 관리자 UI | 서비스 구조 설계, 채널 정책 표준화, 권한 모델링, 셋업 자동화 | + +### 12.4 구현 우선순위 제안 + +1. `BARON-SSO 연동 + 사용자 피드백 작성 페이지 + workspace 매핑 + ABC 진입 제어`를 먼저 구현해야 함. +2. 그 다음 `support_tickets + approvals + abc_feedback_mappings` 중심의 내부 제어 DB를 구축해야 함. +3. 이후 `ABC 멤버/역할 동기화 + 승인 워크플로우 + 외부 연동`을 확장하는 순서가 가장 현실적임. +4. 마지막으로 `마이그레이션 도구 + 통계/리포트 + 셋업 자동화`를 붙여 운영 전환을 마무리하는 구성이 적절함. + +## 13. 사용자 작성부터 이슈 생성 및 처리, 답변 입력까지의 시스템 데이터 처리 모식도 + +### 13.1 목적 + +이 절은 일반 사용자가 사용자 피드백 작성 페이지에서 문의를 등록한 뒤, 담당자가 이슈를 생성하고 처리한 후 답변 또는 처리 결과를 입력할 때까지 데이터가 어느 시스템에 저장되고 어떻게 연결되는지를 한 번에 보여주기 위한 보조 모식도임. + +핵심 확인 포인트는 다음과 같음. + +- 일반 사용자는 보통 BARON-SSO 인증을 마치고, 우리 시스템이 접근 권한과 연결 채널을 판정한 뒤 작성 페이지에 진입함. +- 운영 담당자와 관리자도 동일하게 BARON-SSO 인증 또는 세션 확인을 거친 뒤, 역할에 맞는 운영 화면 또는 관리자 화면으로 분기 진입함. +- 사용자 입력 원문은 ABC User Feedback에 저장됨. +- 제어용 상태와 승인/운영 메타데이터는 우리 DB에 저장됨. +- 담당자 이슈 생성과 처리 이력은 ABC 이슈와 우리 DB가 함께 관리함. +- 최종 답변 또는 처리 결과는 사용자에게 보이는 원문/상태 화면으로 다시 동기화됨. + +### 13.2 단계별 데이터 처리 요약 + +| 단계 | 수행 주체 | 주요 처리 | 주요 저장 위치 | +| --- | --- | --- | --- | +| 1 | 일반 사용자 | 신청/문의 메뉴로 진입하고 보호된 작성 페이지 접근을 시도 | 진입 전 상태는 포털 또는 앱 컨텍스트 | +| 2 | BARON-SSO + 우리 시스템 | 로그인 세션 확인 또는 SSO 인증 수행, `user_id`, `tenant_id` 확보 | 우리 시스템 세션 / 인증 컨텍스트 | +| 3 | 우리 시스템 | `workspace`, 채널, 권한, 요청 유형을 판정하고 작성 가능한 대상인지 검증 | 우리 시스템 제어 로직 | +| 4 | 일반 사용자 | 권한이 확인된 작성 페이지에서 문의/신청 내용을 작성하고 제출 | 제출 전 일시 상태는 우리 시스템 화면 메모리 | +| 5 | 우리 시스템 | ABC 저장 API 호출 | 우리 시스템 제어 로직 | +| 6 | ABC User Feedback | 피드백 원문, 필드값, 첨부, 생성된 `feedback_id` 저장 | ABC Feedback 원본 저장소 | +| 7 | 우리 시스템 | `feedback_id`를 받아 내부 티켓, 승인 필요 여부, 상태값 생성 | 우리 DB `support_tickets`, `abc_feedback_mappings` | +| 8 | 운영 담당자/관리자 | 공통 업무 진입점 또는 운영 메뉴로 접속하고 보호된 운영 화면 접근을 시도 | 운영 포털 또는 운영 메뉴 컨텍스트 | +| 9 | BARON-SSO + 우리 시스템 | 로그인 세션 확인 또는 SSO 인증 수행 후 운영 권한, 관리자 권한, 조회 가능한 `workspace` 범위 판정 | 우리 시스템 세션 / 권한 컨텍스트 | +| 10 | 승인자/담당자/관리자 | 역할에 따라 분기된 운영 화면 또는 ABC 관리자 UI에서 승인, 조회, 이슈 작업 수행 | 우리 시스템 운영 화면 + ABC 관리자 UI | +| 11 | 승인자/담당자 | 승인 또는 반려 판단 | 우리 DB `request_approvals`, `support_tickets` | +| 12 | 담당자/관리자 | ABC 관리자 UI에서 이슈 생성 및 피드백-이슈 연결 | ABC Issue, ABC Feedback-Issue 연결 정보 | +| 13 | 우리 시스템 | 담당자 배정, 처리 메모, 외부 연동 결과, SLA 상태 저장 | 우리 DB `support_tickets`, 운영 이력, 알림 이력 | +| 14 | 담당자/관리자 | 처리 결과 또는 답변 입력, 필요 시 원문/상태 반영 | ABC 원문 표시 정보 + 우리 DB 상태값 | +| 15 | 일반 사용자 | ABC 조회 화면 또는 우리 시스템 조회 화면에서 처리 결과 확인 | ABC 원문 + 우리 DB 상태 동기화 결과 | + +### 13.3 End-to-End 데이터 흐름 모식도 + +```mermaid +flowchart LR + classDef result fill:#eef6ff,stroke:#1d4ed8,stroke-width:1.5px,color:#0f172a; + classDef control fill:#f8fafc,stroke:#334155,stroke-width:1.2px,color:#111827; + classDef external fill:#fff7ed,stroke:#c2410c,stroke-width:1.2px,color:#111827; + + U[일반 사용자] + OP[운영 담당자] + AD[관리자/승인자] + E1[신청/문의 메뉴 진입\n포털 또는 앱]:::result + E2[운영/관리 메뉴 진입\n운영 포털 또는 관리자 메뉴]:::result + A0[BARON-SSO 인증 / 세션 확인]:::external + G0[우리 시스템\n사용자/운영자/관리자 권한 판정\nworkspace / 채널 / 역할 결정]:::control + W[사용자 화면 분기\n작성 / 내 목록 / 내 상세]:::result + A1[ABC Feedback 저장 API]:::control + A2[ABC 원본 피드백 저장소\n본문/필드값/첨부/feedback_id]:::result + D1[우리 DB\nsupport_tickets\nabc_feedback_mappings]:::result + O0[운영 화면 분기\n운영 목록 / 승인 목록 / 상세]:::result + AP[승인 처리 엔진\n승인/반려/상태 전이]:::control + D2[우리 DB\nrequest_approvals\nstatus history]:::result + O1[ABC 관리자 UI]:::result + I1[ABC Issue 저장소\nissue / feedback-issue link]:::result + O2[우리 시스템\n담당자 배정/처리 메모/외부 연동]:::control + D3[우리 DB\n운영 이력 / 알림 / SLA / 후속조치]:::result + X1[자산/차량/Naver Works/SMS]:::external + A3[결과 반영 화면\nABC 원문 + 우리 시스템 상태 동기화]:::result + R1[최종 사용자 조회 화면]:::result + + U -->|신청/문의 메뉴 선택| E1 + OP -->|운영 업무 진입| E2 + AD -->|관리/승인 업무 진입| E2 + E1 -->|로그인 또는 기존 세션 확인| A0 + E2 -->|로그인 또는 기존 세션 확인| A0 + A0 -->|user_id, tenant_id, role context 확보| G0 + G0 -->|일반 사용자 화면 진입| W + W -->|작성 완료 후 제출| A1 + A1 -->|피드백 원문 저장| A2 + A2 -->|feedback_id 반환 및 내부 매핑 시작| D1 + D1 -->|운영/관리 대상 건 생성| O0 + G0 -->|운영자/관리자 화면 진입| O0 + O0 -->|승인 필요 건은 승인 엔진으로 전달| AP + AP -->|승인/반려 이력 기록| D2 + D2 -->|승인 결과 반영| D1 + O0 -->|원문 확인 및 관리자 기능 진입| O1 + O1 -->|이슈 생성/연결 요청| I1 + I1 -->|이슈 식별자/연결 정보 반환| O2 + O2 -->|담당자 배정, 처리 메모, 상태 갱신| D3 + O2 -->|support_tickets 상태 갱신| D1 + O2 -->|외부 업무 시스템 호출| X1 + O1 -->|답변/처리 결과 입력| O2 + D1 -->|최종 상태 동기화| A3 + D3 -->|처리 결과 동기화| A3 + O2 -->|답변/처리 결과 반영| A3 + A3 -->|내 목록 / 상세 / 결과 조회 제공| R1 +``` + +### 13.4 저장 책임 정리 + +| 데이터 유형 | 우선 저장 위치 | 설명 | +| --- | --- | --- | +| 사용자 입력 본문, 필드값, 첨부 | ABC User Feedback | 원문 데이터의 기준 저장소 | +| `feedback_id`와 내부 티켓 연결 | 우리 DB | `support_tickets`, `abc_feedback_mappings`에 저장 | +| 승인 상태, 반려 이력, 단계별 결재 결과 | 우리 DB | `request_approvals`, 상태 이력 테이블에 저장 | +| 운영 이슈, 피드백-이슈 연결 | ABC User Feedback | 운영 추적의 기준 이슈 저장소 | +| 담당자 배정, 처리 메모, SLA, 외부 연동 결과 | 우리 DB | 운영 제어와 리포팅을 위한 내부 메타데이터 | +| 사용자에게 보여줄 최종 처리 결과 | ABC 원문 조회 화면 + 우리 DB 동기화 결과 | 원문과 상태를 함께 보여주는 최종 표시 데이터 | + +### 13.5 설계 해석 포인트 + +- 사용자 작성은 인증과 접근 권한 검증, `workspace`-채널 판정이 끝난 뒤 시작되며, 작성 UX와 진입 제어는 우리 시스템이 담당하지만 원문 자체는 ABC에 남김. +- 승인과 운영 처리 단계에서는 우리 DB가 상태 제어의 기준 시스템 역할을 수행함. +- 담당자의 이슈 생성은 ABC를 기준으로 하되, 그 이슈를 어떤 업무 상태로 해석할지는 우리 시스템이 담당함. +- 답변 입력 또는 처리 결과 입력은 단순 텍스트 등록이 아니라 `support_tickets` 상태, 승인 결과, 운영 메모, 외부 연동 결과를 함께 묶어 사용자에게 노출하는 흐름으로 봐야 함. \ No newline at end of file diff --git a/docs/관리페이지 md 파일/detail-popup-window-tasks.md b/docs/관리페이지 md 파일/detail-popup-window-tasks.md new file mode 100644 index 0000000..9212ce3 --- /dev/null +++ b/docs/관리페이지 md 파일/detail-popup-window-tasks.md @@ -0,0 +1,20 @@ +# 피드백·이슈 상세 팝업 작업 기록 + +- 작성일: 2026-09-07 +- 목표: 통합관리 화면에서 피드백과 이슈 상세를 별도 창으로 열고, 같은 유형의 상세 창은 하나만 재사용한다. + +## 적용 내용 + +- [x] 피드백 클릭 시 `baron-feedback-detail` 이름의 팝업을 열도록 변경했다. +- [x] 이슈 클릭 시 `baron-issue-detail` 이름의 팝업을 열도록 변경했다. +- [x] 같은 유형을 다시 클릭하면 새 창을 만들지 않고 기존 팝업의 URL과 상세 내용을 바꾼다. +- [x] Chrome Window Management API를 지원하면 오른쪽 디스플레이를 찾아 작업 영역 전체 크기로 이동·리사이즈한다. +- [x] API를 지원하지 않거나 권한을 거부하면 부모 창의 오른쪽 좌표를 기준으로 연다. +- [x] 팝업 전용 URL에서는 목록 대신 상세 패널만 표시한다. +- [x] 팝업 전용 상세 패널의 배경 오버레이·열기 애니메이션을 제거한다. +- [x] 대시보드, 피드백 목록·칸반, 이슈 목록·칸반에서 동일 동작을 사용한다. +- [x] Web typecheck 및 lint를 통과했다. + +## 브라우저 제약 + +Chrome은 Window Management 권한이 허용된 경우 연결된 화면 정보를 제공한다. 그 경우 오른쪽 디스플레이를 명시적으로 선택한다. 권한을 거부하거나 기능을 지원하지 않는 브라우저는 부모 창 오른쪽 좌표 요청으로 동작한다. diff --git a/docs/관리페이지 md 파일/distributed-system-and-pk-design-options.md b/docs/관리페이지 md 파일/distributed-system-and-pk-design-options.md new file mode 100644 index 0000000..ff3db9b --- /dev/null +++ b/docs/관리페이지 md 파일/distributed-system-and-pk-design-options.md @@ -0,0 +1,560 @@ +# 분산 시스템 도입 시점 및 PK 설계안 비교 + +## 문서 목적 + +본 문서는 다중 프로젝트·다중 RP에서 Q&A와 피드백을 수집하는 시스템을 기준으로 다음 두 가지 설계를 비교한다. + +1. 처음부터 분산 시스템을 고려하여 설계하는 방안 +2. 현재 중앙 DB 구조를 유지하고, 실제 병목 발생 시 분산 시스템으로 확장하는 방안 + +현재 저장소는 `apps/api → 중앙 MySQL` 구조이며, 공통 엔터티의 PK는 `INT AUTO_INCREMENT`이다. 스테이징 덤프의 `feedbacks`도 약 523건 수준이므로 현재는 PK 용량이나 DB 하드웨어가 병목인 단계가 아니다. + +--- + +## 핵심 결론 + +| 구분 | 1안: 처음부터 분산 고려 | 2안: 단계적 분산 도입 | +|---|---|---| +| 내부 PK | `BINARY(16)` UUID v7 | `BIGINT UNSIGNED AUTO_INCREMENT` | +| 외부 식별자 | PK와 동일한 UUID v7 | 별도 `public_id` UUID v7 | +| 다중 작성 서버 | 즉시 지원 | API에서 중앙 수집, 필요 시 확장 | +| 초기 비용 | 높음 | 낮음 | +| FK·인덱스 크기 | 큼 | 작음 | +| 향후 샤딩 | 쉬움 | `public_id`를 기준으로 단계적 전환 | +| 현재 시스템 적합성 | 미래 요구사항이 확정된 경우 | 현재 시스템에 권장 | + +최종 추천은 **2안**이다. + +```text +내부 PK BIGINT UNSIGNED AUTO_INCREMENT +외부 ID UUID v7 → BINARY(16) +멱등 키 source_system + source_record_id UNIQUE +검색 MySQL 인덱스 + 필요 시 OpenSearch +분산 도입 실제 Primary 병목 발생 시 단계별 확장 +``` + +--- + +## 연간 5만 건 기준 하드웨어 검토 + +연간 피드백·댓글·이슈 등 전체 업무 레코드를 **5만 건**으로 가정한다. + +| 항목 | 평균 규모 | +|---|---:| +| 연간 레코드 | 50,000건 | +| 일평균 | 약 137건 | +| 시간당 평균 | 약 5.7건 | +| 초당 평균 쓰기 | 약 0.0016건 | +| 5년 누적 | 약 250,000건 | +| 10년 누적 | 약 500,000건 | + +연간 5만 건 자체는 단일 MySQL Primary가 감당하지 못할 규모가 아니다. 데이터가 특정 이벤트에 몰리는 경우를 고려해도, 평균 쓰기량만으로 분산 DB를 선택할 근거는 부족하다. + +만약 피드백 5만 건과 댓글 5만 건을 각각 의미한다면 연간 총 10만 건이지만, 이 정도 역시 연간 누적량만으로는 분산 DB 도입을 결정할 수준은 아니다. 동시 접속자와 순간 피크 쓰기량을 별도로 측정해야 한다. + +예를 들어 평균 쓰기량의 1,000배가 짧은 시간에 발생해도 약 1.6건/초 수준이다. 다만 신규 글 등록보다 다음 항목이 먼저 병목이 될 수 있다. + +- 동시에 접속한 관리자의 목록·통계 조회 수 +- JSON 필드의 전문 검색과 `%검색어%` 검색 +- 매 요청마다 실행하는 `COUNT(*)` +- OFFSET이 큰 페이지 조회 +- 첨부파일 다운로드 트래픽 +- 알림·웹훅·검색 색인 작업이 본 요청과 같은 트랜잭션에 묶이는 경우 + +첨부파일 본문은 DB 용량 계산에서 제외하고 Object Storage에 저장한다. 본문과 댓글 평균 크기를 2~10KB로 가정하면 5만 건의 원본 데이터는 대략 100~500MB이고, 인덱스·레코드 오버헤드를 포함해도 일반적인 단일 DB에서 수년간 관리 가능한 범위다. 실제 크기는 JSON 본문과 인덱스 수를 측정해 확정해야 한다. + +### 5만 건 기준 권장 구성 + +```text +API 서버 2대 이상 +MySQL Primary 1대 +백업 또는 장애 복구용 Replica 1대 +Object Storage: 첨부파일 +OpenSearch: 전문 검색이 필요할 때만 +``` + +이 규모에서는 샤딩이나 멀티 Primary보다 다음 순서가 적절하다. + +1. `BIGINT UNSIGNED` 내부 PK와 UUID v7 `public_id` 적용 +2. 목록 조회용 복합 인덱스와 Keyset 페이지네이션 적용 +3. JSON 검색을 Generated Column 또는 OpenSearch로 분리 +4. 읽기 부하가 증가하면 Read Replica 추가 +5. 쓰기·알림·검색 색인을 Outbox와 큐로 분리 +6. 실제 Primary 포화 시에만 샤딩 검토 + +따라서 **연간 5만 건을 전제로 하면 2안이 적합**하다. 1안은 연간 건수 때문이 아니라 다중 DB 쓰기, 오프라인 생성, 멀티리전, 강한 장애 격리처럼 분산이 출시 필수 요구사항일 때 선택한다. + +--- + +## PK 변경 공수 비교 + +UUID를 외부 식별자로 추가하는 것과 UUID를 실제 PK로 사용하는 것은 변경 범위가 다르다. + +### 비교표 + +| 항목 | `BIGINT PK` 유지 | `BIGINT PK + UUID public_id` | `UUID v7` 실제 PK | +|---|---|---|---| +| DB PK 변경 | 없음 | 없음 | 모든 PK/FK 변경 | +| FK 컬럼 | `BIGINT` | `BIGINT` | `BINARY(16)` | +| TypeORM Entity | 변경 적음 | 컬럼 추가 | ID 타입·생성 방식 전면 변경 | +| API 계약 | 기존 숫자 ID 유지 | public_id를 단계적 추가 | URL·DTO·응답 ID 변경 | +| 프론트엔드 | 변경 적음 | 필요한 화면부터 변경 | `id: number` 전면 변경 | +| 인덱스 크기 | 가장 작음 | 내부 인덱스는 동일 | PK·FK·보조 인덱스 증가 | +| 기존 데이터 변환 | 없음 | UUID backfill | PK·FK 매핑 및 제약조건 재생성 | +| 외부 연동 영향 | 낮음 | 낮음~중간 | 웹훅·검색·테스트·연동 영향 큼 | +| 분산 선발급 | 어려움 | public_id로 가능 | PK 자체로 가능 | +| 전체 공수 | 낮음 | 중간 | 높음 | + +### UUID를 실제 PK로 변경할 때 필요한 작업 + +현재 시스템은 공통 엔터티에서 `id: number`와 `INT AUTO_INCREMENT`를 사용하고 있으며, 댓글·첨부파일·통계·이슈 연결 테이블이 숫자 ID를 FK로 참조한다. 따라서 UUID PK 전환 시 다음 작업이 필요하다. + +1. 모든 핵심 테이블의 PK를 `BINARY(16)`으로 변경 +2. 모든 FK와 조인 테이블의 참조 컬럼을 `BINARY(16)`으로 변경 +3. TypeORM Entity의 ID 타입과 생성 로직 변경 +4. DTO의 숫자 검증을 UUID 검증으로 변경 +5. API URL과 응답의 ID 형식 변경 +6. 프론트엔드의 `id: number` 타입과 정렬·비교 로직 변경 +7. 기존 Foreign Key와 인덱스 삭제·재생성 +8. 웹훅, OpenSearch 문서 ID, 테스트 fixture, E2E 데이터 변경 +9. UUID 생성·바이너리 변환·JSON 직렬화 규칙 추가 + +`UUID v4`보다 `UUID v7`을 사용하고, 저장 타입은 `CHAR(36)`이 아니라 `BINARY(16)`으로 통일한다. + +### 권장 마이그레이션 방식 + +현재 구조에서는 기존 PK를 유지하면서 외부 UUID를 추가하는 방식이 안전하다. + +```sql +ALTER TABLE feedbacks + ADD COLUMN public_id BINARY(16) NULL, + ADD UNIQUE KEY uk_feedback_public_id (public_id); +``` + +이후 다음 순서로 전환한다. + +1. 모든 기존 행에 UUID v7 `public_id`를 backfill +2. 신규 데이터부터 `public_id`를 동시에 생성 +3. API 응답과 URL에 public_id를 선택적으로 추가 +4. 외부 클라이언트가 public_id를 사용하도록 전환 +5. `source_system + source_record_id` 유일 키 추가 +6. 충분한 호환 기간 후 숫자 ID의 외부 노출을 중단 + +이 방식은 내부 조인과 FK를 그대로 유지하면서도, 향후 API 수평 확장·다중 작성 서버·샤딩에서 UUID를 전역 식별자로 사용할 수 있게 한다. + +### 공수 판단 + +```text +BIGINT PK 유지 낮음 +BIGINT PK + UUID public_id 추가 중간 +UUID v7을 실제 PK로 전면 전환 높음 +``` + +현재 스테이징 데이터가 약 523건이므로 데이터 backfill 자체는 어렵지 않다. 하지만 UUID 실제 PK 전환의 공수는 데이터 건수보다 Entity, FK, API, 프론트엔드, 테스트와 외부 연동 변경에서 발생한다. + +따라서 UUID가 반드시 전역 PK여야 한다는 요구사항이 확정되지 않았다면, `BIGINT 내부 PK + UUID v7 public_id`를 기본 설계로 선택한다. + +--- + +## 외부 식별자 방식 비교 + +`public_id`는 외부 API, URL, 웹훅, 로그에서 사용하는 식별자다. 내부 DB PK와 달리 다음 기준을 함께 고려해야 한다. + +- 여러 서버에서 중앙 조율 없이 생성할 수 있는가 +- 시간순 정렬이 가능한가 +- URL과 API에서 사용하기 쉬운가 +- DB 인덱스에 미치는 영향이 작은가 +- 표준과 라이브러리 지원이 충분한가 +- 생성 시간이나 순번이 과도하게 노출되지 않는가 + +| 방식 | 표현 길이 | 시간순 정렬 | 분산 생성 | 운영 복잡도 | 주요 단점 | 적합성 | +|---|---:|:---:|:---:|:---:|---|---| +| UUID v7 | 36자 문자열 / 16바이트 | 가능 | 가능 | 낮음 | 생성 시간이 일부 노출됨 | 가장 균형적 | +| ULID | 26자 문자열 / 16바이트 | 가능 | 가능 | 낮음 | UUID 표준과 별도 사양 | 짧은 URL | +| Snowflake | 약 18~19자리 | 가능 | 가능 | 높음 | Worker ID와 시계 관리 필요 | 초고속 분산 시스템 | +| UUID v4 | 36자 문자열 / 16바이트 | 불가 | 가능 | 낮음 | 랜덤 삽입, 정렬 정보 없음 | 랜덤성과 추측 방지 | + +### UUID v7 + +UUID v7은 IETF RFC 9562에 정의된 표준 UUID 방식이다. 앞부분에 Unix millisecond timestamp를 포함하고 나머지 영역을 랜덤 또는 단조 증가 값으로 사용할 수 있다. [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562.html) + +```text +0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d +``` + +장점: + +- UUID v4와 동일한 128비트 구조 및 `BINARY(16)` 저장 방식 사용 +- UUID v4보다 B-Tree 인덱스의 순차 삽입에 유리 +- Worker ID를 별도로 배정하지 않아도 됨 +- UUID 표준과 도구를 그대로 활용 가능 +- 외부 ID로 사용해도 충돌 가능성이 매우 낮음 + +주의사항: + +- 생성 시간이 밀리초 단위로 일부 노출됨 +- 보안 토큰이나 비밀번호 재설정 토큰으로 사용하면 안 됨 +- UUID 자체가 접근 권한을 대신하지 않음 + +### ULID + +ULID는 128비트 시간순 식별자이며, 표준 표현이 26자이고 Crockford Base32를 사용한다. UUID 문자열보다 짧고 URL에 넣기 편하다. [ULID 공식 사양](https://github.com/ulid/spec) + +```text +01K8A9V4N5J9F28D5G3H1A2B3C +``` + +UUID v7과 기능적으로 매우 유사하지만, UUID 표준과는 별도의 사양이다. 외부 URL의 길이와 사람이 읽기 쉬운 형태를 가장 중요하게 생각하면 ULID가 더 적합할 수 있다. + +### Snowflake + +Snowflake는 보통 다음 정보를 조합한 64비트 숫자 ID다. + +```text +timestamp + worker ID + sequence +``` + +인덱스와 FK가 UUID보다 작고 숫자 정렬도 빠르다. 그러나 서버별 Worker ID 할당, 오토스케일링 시 충돌 방지, 시계 역행 처리, JavaScript에서의 큰 정수 직렬화가 필요하다. [X의 Snowflake ID 설명](https://docs.x.com/fundamentals/x-ids) + +연간 5만~10만 건 수준의 시스템에서는 저장 공간 절약보다 운영 복잡도가 더 커질 가능성이 높다. + +### UUID v4 + +UUID v4는 랜덤성이 가장 중요한 경우에 적합하다. + +```text +c9a646d3-9c61-4cc9-bc19-482386a37365 +``` + +시간 정보가 없으므로 생성 순서로 정렬할 수 없고, 랜덤한 B-Tree 삽입으로 UUID v7보다 인덱스 효율이 떨어질 수 있다. 생성 시점 노출을 피해야 하는 외부 토큰에는 UUID v4가 더 적합할 수 있다. + +### 이 시스템의 외부 식별자 선택 + +현재 시스템에서는 다음 구성이 적절하다. + +```text +DB 저장 BINARY(16) +생성 방식 UUID v7 +API 응답 표준 UUID 문자열 +URL UUID 문자열 +화면 표시 축약 UUID + 복사 버튼 +내부 조인 BIGINT PK +``` + +사람이 업무 화면에서 확인할 번호가 필요하다면 UUID를 표시 번호로 사용하지 않고 별도의 업무 번호를 둔다. + +```text +외부 식별자: 0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d +업무 표시번호: ABC-2026-000123 +``` + +최종 선택 기준: + +| 우선순위 | 선택 | +|---|---| +| 표준성·분산성·시간순 정렬의 균형 | UUID v7 | +| 짧은 URL과 가독성 | ULID | +| 초당 수천~수만 건의 분산 숫자 생성 | Snowflake | +| 시간 노출 없는 랜덤성 | UUID v4 | + +따라서 이 시스템은 `BIGINT 내부 PK + UUID v7 public_id` 조합을 기본으로 하고, URL 길이가 최우선인 외부 서비스에서만 ULID를 대안으로 검토한다. + +--- + +# 1안. 처음부터 분산 시스템을 고려하는 설계 + +## 1.1 목표 + +다음 요구사항이 이미 확정되어 있을 때 선택한다. + +- 프로젝트별 작성 서버가 중앙 DB 장애와 독립적으로 ID를 생성해야 함 +- 네트워크 단절 후 재시도에도 중복 등록을 방지해야 함 +- 향후 여러 DB 샤드 또는 멀티리전 쓰기를 계획하고 있음 +- 피드백·댓글·이슈가 서로 다른 저장소에 분산될 수 있음 +- 외부 시스템과의 전역 식별자가 필요함 + +## 1.2 권장 ID 구조 + +모든 핵심 엔터티의 PK를 UUID v7 기반 `BINARY(16)`으로 통일한다. + +```text +feedback.id BINARY(16) PRIMARY KEY +feedback_comment.id BINARY(16) PRIMARY KEY +issue.id BINARY(16) PRIMARY KEY +attachment.id BINARY(16) PRIMARY KEY +``` + +UUID는 DB에서 생성하지 않고 작성 애플리케이션에서 생성한다. + +```text +[RP A 서버] ─┐ +[RP B 서버] ─┼─ UUID v7 생성 → 중앙 API → DB 또는 메시지 큐 +[RP C 서버] ─┘ +``` + +같은 요청을 재전송할 때 작성 서버가 최초 생성한 UUID를 다시 보내면 중앙 API에서 멱등 처리를 할 수 있다. + +## 1.3 권장 테이블 구조 + +```sql +CREATE TABLE feedbacks ( + id BINARY(16) NOT NULL, + channel_id BINARY(16) NOT NULL, + data JSON NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + + source_system VARCHAR(32) NULL, + source_record_id VARCHAR(128) NULL, + + PRIMARY KEY (id), + UNIQUE KEY uk_feedback_source (source_system, source_record_id), + KEY idx_feedback_channel_created (channel_id, created_at DESC, id DESC) +); +``` + +`source_system`과 `source_record_id`는 외부 RP의 원본 식별자다. UUID가 동일하지 않더라도 같은 원본 요청의 재전송을 차단할 수 있도록 별도 유일 키를 둔다. + +## 1.4 서비스 구성 + +```text + ┌─ API 인스턴스 1 ─┐ +[RP 서버들] ─────┼─ API 인스턴스 2 ─┼─ [메시지 큐/Outbox] ─ [MySQL Primary] + └─ API 인스턴스 N ─┘ │ + ├─ [Read Replica] + ├─ [OpenSearch] + └─ [Object Storage] +``` + +구성 원칙: + +- API 서버는 상태를 보유하지 않고 수평 확장한다. +- 저장 완료와 알림·검색 색인 작업을 분리한다. +- DB 트랜잭션 안에서 Outbox 이벤트를 저장한 뒤 비동기 발행한다. +- 첨부파일 본문은 DB가 아니라 S3/R2 호환 스토리지에 저장한다. +- 검색과 원본 데이터 저장을 분리한다. +- 샤딩 이후에는 서로 다른 DB 간 Foreign Key를 사용하지 않는다. + +## 1.5 장점 + +- DB에 저장하기 전에도 전역 ID를 발급할 수 있다. +- 네트워크 재시도와 비동기 수집에 유리하다. +- 여러 DB 샤드에 데이터를 나누어도 ID 충돌이 없다. +- 프로젝트 서버가 추가되어도 Worker ID를 배정할 필요가 없다. +- 화면·API·로그에서 같은 식별자를 사용할 수 있다. + +## 1.6 단점 + +- 모든 PK와 FK가 16바이트가 되어 인덱스와 Buffer Pool 사용량이 증가한다. +- TypeORM 엔터티, DTO, 프론트엔드의 `number` 타입을 전반적으로 변경해야 한다. +- UUID 변환 코드와 직렬화 규칙이 필요하다. +- 분산 트랜잭션, 이벤트 중복, 순서 보장 문제를 초기에 해결해야 한다. +- 실제 트래픽이 작으면 운영 복잡도만 증가할 수 있다. + +## 1.7 이 안을 선택해야 하는 조건 + +다음 중 하나라도 출시 전에 확정되어 있다면 1안을 검토한다. + +- 초기부터 여러 DB에 쓰기 작업을 분산해야 함 +- 모바일·현장·오프라인 환경에서 중앙 DB 없이 글을 생성해야 함 +- 리전별 데이터 저장 또는 테넌트별 DB 격리가 필수임 +- 초기부터 글로벌 단일 식별자가 외부 계약으로 고정되어야 함 +- 향후 PK 변경을 절대로 허용할 수 없음 + +하드웨어 용량 때문에 1안을 선택하려면 연간 건수가 아니라 예상 피크 부하를 기준으로 판단한다. 부하 테스트에서 단일 Primary의 CPU·I/O·Lock·응답 시간이 목표치를 초과하고, 수직 확장·인덱스·읽기 복제·비동기화로도 해결되지 않는다는 결과가 있어야 한다. + +단순히 “언젠가 분산할 수 있다”거나 “연간 5만 건이 쌓인다”는 이유만으로 1안을 선택하는 것은 권장하지 않는다. + +--- + +# 2안. 현재 구조 유지 후 병목 시 분산 시스템 도입 + +## 2.1 기본 설계 + +현재 중앙 MySQL 구조를 유지하되, 내부 PK와 외부 식별자를 분리한다. + +```text +내부 PK: BIGINT UNSIGNED AUTO_INCREMENT +외부 public_id: UUID v7 BINARY(16) +외부 원본 키: source_system + source_record_id +``` + +예시: + +```sql +CREATE TABLE feedbacks ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + public_id BINARY(16) NOT NULL, + channel_id BIGINT UNSIGNED NOT NULL, + data JSON NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + + source_system VARCHAR(32) NULL, + source_record_id VARCHAR(128) NULL, + + PRIMARY KEY (id), + UNIQUE KEY uk_feedback_public_id (public_id), + UNIQUE KEY uk_feedback_source (source_system, source_record_id), + KEY idx_feedback_channel_list (channel_id, created_at DESC, id DESC) +); +``` + +댓글·첨부파일·이슈 연결 테이블의 내부 FK는 `BIGINT UNSIGNED`로 통일한다. + +## 2.2 단계별 도입 순서 + +### 단계 0. 단일 DB 최적화 + +- `INT`를 신규 설계부터 `BIGINT UNSIGNED`로 통일한다. +- `feedbacks(channel_id, deleted_at, id)` 복합 인덱스를 추가한다. +- OFFSET 페이지네이션을 Keyset 페이지네이션으로 변경한다. +- JSON에서 자주 검색하는 필드를 일반 컬럼 또는 Generated Column으로 분리한다. +- 목록 조회와 `COUNT(*)`를 분리하거나 count 캐시를 사용한다. +- `%검색어%` 전문 검색은 OpenSearch로 이전한다. + +권장 목록 조회: + +```sql +SELECT ... +FROM feedbacks +WHERE channel_id = ? + AND deleted_at IS NULL + AND id < ? +ORDER BY id DESC +LIMIT 20; +``` + +### 단계 1. 읽기 확장 + +다음 순서로 읽기 부하를 분산한다. + +```text +MySQL Primary + ├─ Read Replica 1 + └─ Read Replica 2 +``` + +- 쓰기·트랜잭션은 Primary로 보낸다. +- 목록·통계·검색용 조회는 Replica 또는 OpenSearch로 보낸다. +- 작성 직후 조회처럼 최신성이 필요한 요청은 Primary를 사용한다. +- Replica 지연을 모니터링한다. + +### 단계 2. 애플리케이션 수평 확장 + +- API 서버를 무상태로 유지한다. +- 세션은 외부 저장소 또는 서명 토큰으로 관리한다. +- Connection Pool을 서버 수에 맞게 제한한다. +- 알림·웹훅·검색 색인은 큐 또는 Outbox로 분리한다. + +### 단계 3. 쓰기 부하 분리 + +Primary 쓰기가 포화되기 시작하면 바로 샤딩하지 않고 다음 작업부터 수행한다. + +- 통계 집계를 원본 트랜잭션과 분리한다. +- 알림·웹훅·검색 색인 작업을 비동기화한다. +- 이력·이벤트 테이블을 별도 저장소로 이동한다. +- 오래된 데이터는 보관 DB 또는 파티션으로 이동한다. +- 쓰기 트랜잭션의 Lock 범위와 인덱스를 점검한다. + +### 단계 4. 실제 필요 시 샤딩 + +샤딩 키는 `tenant_id`, `workspace_id`, `project_id` 중 실제 데이터 격리 기준을 선택한다. + +```text +Shard Map +tenant/workspace → shard-1 또는 shard-2 +``` + +이때 `public_id`는 샤드와 관계없이 전역 식별자로 유지한다. 샤드 간 JOIN과 Foreign Key는 사용하지 않고, API나 이벤트 계층에서 조회를 조합한다. + +## 2.3 장점 + +- 현재 시스템의 변경 범위가 작다. +- `BIGINT` PK와 FK로 인덱스·조인 성능을 유지한다. +- 실제 병목에 맞춰 읽기·검색·쓰기 분산을 각각 도입할 수 있다. +- UUID v7 `public_id`를 지금 추가하므로 향후 샤딩 시 외부 식별자 변경이 없다. +- 초기 운영과 장애 대응이 단순하다. + +## 2.4 단점 + +- 내부 `BIGINT AUTO_INCREMENT`는 여러 DB에서 독립적으로 발급하기 어렵다. +- 샤딩 시 내부 ID는 전역 식별자로 사용할 수 없다. +- 향후 샤딩 시 DB 간 Foreign Key를 제거해야 한다. +- 처음부터 다중 DB 쓰기가 필수라면 추가 전환 작업이 필요하다. + +## 2.5 분산 도입 임계점 + +다음 조건이 피크 시간에 지속되고, 인덱스·쿼리·캐시·수직 확장으로 해결되지 않을 때 다음 단계로 이동한다. + +| 측정 항목 | 검토 기준 | +|---|---:| +| DB CPU | 70~80% 이상 지속 | +| 디스크 I/O 대기 | 10% 이상 | +| 읽기 p95 | 300ms 이상 | +| 쓰기 p95 | 500ms 이상 | +| Connection Pool | 80% 이상 | +| Buffer Pool 적중률 | 99% 미만 | +| Replica 지연 | 5~10초 이상 | +| Lock wait/deadlock | 지속적인 증가 | +| Primary 쓰기 용량 | 부하 테스트 최대치의 70~80% 초과 | + +행 수는 참고 지표일 뿐 분산 도입의 직접 기준으로 사용하지 않는다. + +--- + +# PK 자료 유형 비교 + +| 방식 | 저장 | 중앙 DB 의존 | 분산 선발급 | 인덱스 효율 | 권장 용도 | +|---|---|---:|---:|---:|---| +| `INT AUTO_INCREMENT` | 4바이트 | 높음 | 불가 | 매우 좋음 | 기존 소규모 시스템 | +| `BIGINT UNSIGNED AUTO_INCREMENT` | 8바이트 | 높음 | 불가 | 매우 좋음 | 현재 중앙 DB 내부 PK | +| UUID v4 `BINARY(16)` | 16바이트 | 없음 | 가능 | 보통 | 보안 우선 식별자 | +| UUID v7 `BINARY(16)` | 16바이트 | 없음 | 가능 | 좋음 | 분산·외부 식별자 | +| ULID | 16바이트 또는 문자열 | 없음 | 가능 | 좋음 | UUID v7 대안 | +| Snowflake `BIGINT` | 8바이트 | 없음 | 가능 | 매우 좋음 | 초고속 분산 숫자 ID | +| `CHAR(36)` UUID | 36바이트 | 없음 | 가능 | 낮음 | 비권장 | + +`UUID v4`, `UUID v7`, `ULID`를 사용하더라도 문자열로 저장하지 말고 가능하면 `BINARY(16)`으로 저장한다. + +--- + +# 두 안의 최종 선택 기준 + +## 1안을 선택하는 경우 + +```text +초기부터 다중 DB 쓰기 +또는 오프라인 생성 +또는 멀티리전 저장 +또는 전역 UUID PK가 외부 계약으로 확정 +``` + +이 경우 `UUID v7 BINARY(16)`을 실제 PK로 사용한다. + +## 2안을 선택하는 경우 + +```text +현재는 중앙 MySQL +쓰기량이 크지 않음 +운영 복잡도를 낮춰야 함 +향후 분산 가능성만 있음 +``` + +이 경우 `BIGINT UNSIGNED`를 내부 PK로 사용하고 `UUID v7 BINARY(16)`을 외부 식별자로 추가한다. + +## 최종 추천 + +현재 시스템에는 2안을 적용한다. + +```text +feedbacks.id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY +feedbacks.public_id BINARY(16) UNIQUE NOT NULL +feedbacks.source_system +feedbacks.source_record_id +UNIQUE(source_system, source_record_id) +``` + +이 설계는 현재의 인덱스·조인 성능을 유지하면서도, 향후 API 수평 확장·Read Replica·OpenSearch·Outbox·샤딩으로 확장할 수 있는 경로를 확보한다. + +1안은 분산 요구사항이 기술적 가능성이 아니라 **출시 전 확정된 필수 요구사항**일 때 선택한다. diff --git a/docs/관리페이지 md 파일/egbim_to_secretary_staging_migration_tasks.md b/docs/관리페이지 md 파일/egbim_to_secretary_staging_migration_tasks.md new file mode 100644 index 0000000..2d980c2 --- /dev/null +++ b/docs/관리페이지 md 파일/egbim_to_secretary_staging_migration_tasks.md @@ -0,0 +1,313 @@ +# EGBIM QA 데이터 스테이징 이관 작업 목록 + +> 기준 원본: `scripts/egbim_qa.sql` +> 대상 환경: 스테이징의 `baron_support` 및 필요한 경우 `userfeedback` +> 상태: 설계·준비 단계 +> 원칙: 이 문서는 작업 목록이며, 아직 DB 이관을 실행하지 않는다. + +## 1. 이관 범위 + +`egbim_qa.sql`은 EGBIM 기존 QA 시스템의 게시글, 댓글, 게시글 첨부파일, 댓글 이미지 데이터 덤프다. 원본 SQL을 스테이징의 `baron_support`에 그대로 실행하지 않고, Secretary의 표준 티켓·댓글·첨부 구조로 변환한다. + +원본 덤프에서 확인된 주요 테이블과 데이터 규모는 다음과 같다. + +| 원본 테이블 | 용도 | 덤프 기준 규모 | 주요 관계 | +| --- | --- | ---: | --- | +| `qa_posts` | QA 게시글 | 427건 | 게시글의 루트 레코드 | +| `qa_comments` | 게시글 댓글 | 677건 | `post_id -> qa_posts.post_id` | +| `qa_attachments` | 게시글 첨부파일 | 194건 | `post_id -> qa_posts.post_id` | +| `qa_comment_images` | 댓글 이미지·썸네일 | 42건 | `comment_id -> qa_comments.comment_id` | + +실제 실행 전에는 SQL을 파싱하는 사전 점검 스크립트로 행 수와 고아 관계를 다시 계산한다. 위 숫자는 이관 계획 수립용 기준값이다. + +## 2. 대상 구조 및 기본 원칙 + +### 2.1 대상 시스템 + +- BARON-SSO: 사용자 식별자와 테넌트의 원본 +- `baron_support`: workspace, 사용자 접근권한, 표준 티켓, 댓글, 첨부, 외부 시스템 매핑의 원본 +- `userfeedback`: ABC User Feedback의 게시글·댓글·첨부 원본 및 콘솔 기능 저장소 + +이번 EGBIM 이관에서는 권한을 `userfeedback.users.type`, `userfeedback.roles`, `userfeedback.members`에 새로 만들지 않는다. 권한과 사용자 연결은 `baron_support`의 `support_users`, `support_roles`, `support_role_assignments`, `user_workspace_access` 정책을 따른다. + +### 2.2 절대 금지 사항 + +- `egbim_qa.sql` 전체를 `baron_support`에 그대로 import하지 않는다. +- EGBIM의 숫자 ID를 대상 테이블의 ID로 재사용하지 않는다. +- `login_id`, 이메일, 전화번호를 BARON-SSO `sso_subject`로 임의 사용하지 않는다. +- 원본의 절대 파일 경로를 그대로 웹 URL이나 `storage_key`로 노출하지 않는다. +- 기존 스테이징 데이터와 파일을 백업하지 않은 상태에서 본 이관을 실행하지 않는다. + +## 3. Workspace와 ABC 매핑 선행 작업 + +기존 설계 문서의 초기 운영 구조에 맞춰 EGBIM은 다음 workspace로 연결한다. + +| 항목 | 계획값 | 확정 필요 | +| --- | --- | --- | +| Secretary workspace code | `EGBIM` | 예 | +| Secretary workspace name | `EGBIM` | 예 | +| Secretary 기본 채널 | `EGBIM` | 예 | +| ABC project | EGBIM 프로젝트 | 실제 `project_id` 확인 | +| ABC channel | EGBIM 기본 채널 | 실제 `channel_id` 확인 | +| 원본 시스템 코드 | `EGBIM_QA` | 예 | + +작업 순서: + +- [ ] 스테이징 `baron_support.workspaces`에서 `EGBIM` workspace 존재 여부 확인 +- [ ] EGBIM workspace의 기본 채널 및 ABC `project_id/channel_id` 확인 +- [ ] `workspace_channel_mappings`에 workspace·ABC 프로젝트·채널 매핑 저장 +- [ ] 기존 ABC에 이미 이관된 EGBIM 데이터가 있는지 제목, 생성일, 원본 ID 메타데이터로 중복 확인 +- [ ] 기존 ABC 데이터와 중복되는 경우 `abc_feedback_mappings`를 우선 연결하고 재생성하지 않을 기준 확정 + +## 4. 원본-대상 매핑 + +### 4.1 게시글 + +| EGBIM 원본 | Secretary 대상 | 매핑 규칙 | +| --- | --- | --- | +| `qa_posts.post_id` | `migration_mappings.source_entity_id` | 원본 게시글 ID로 보존 | +| `qa_posts` 1행 | `support_tickets` 1행 | `source_system = EGBIM_QA` | +| `login_id` 또는 확인된 SSO subject | `support_tickets.requester_id` | identity resolver 결과만 사용 | +| 원본 테넌트 | `requester_tenant_id` | EGBIM SSO tenant 확정 후 저장 | +| `user_id` | `requester_contact` 또는 `extra_fields` | 숫자/레거시 사용자 키로 보존 | +| `user_name` | `requester_name` | 없으면 SSO profile 조회 결과로 보완 | +| `phone` | `requester_phone_number` | 개인정보 접근·마스킹 정책 확인 | +| `department` | `requester_department` | 원본 값 보존 | +| `title` | `title` | 길이 255자 초과 시 원본을 `extra_fields`에 보존 | +| `content` | `description` | HTML/줄바꿈 변환 규칙을 적용 | +| `category` | `category_code` 또는 `extra_fields.category` | 표준 코드가 없으면 원본값 보존 후 매핑 | +| `is_secret` 또는 `is_private` | `support_tickets.is_secret` | 둘 중 하나라도 true면 비밀글; ABC 이관본의 `additional_data.is_secret`에도 원본 플래그 보존 | +| `is_internal` | `requires_approval`가 아님 | 내부 공개범위 필드로 별도 보존; 승인 여부와 혼동 금지 | +| `created_at`, `updated_at` | 동일 대상 시간 필드 | 타임존을 명시하여 변환 | + +`attachment`, `complete_form`, `is_read_admin`, `company`, `family_company`, `position` 등 Secretary에 직접 대응하지 않는 값은 버리지 않고 `support_tickets.extra_fields`에 원본 필드명으로 보존한다. + +### 4.2 댓글 + +| EGBIM 원본 | Secretary 대상 | 매핑 규칙 | +| --- | --- | --- | +| `qa_comments.comment_id` | `migration_mappings.source_entity_id` | 댓글 ID로 보존 | +| `qa_comments.post_id` | `ticket_comments.ticket_id` | 게시글 매핑을 먼저 조회 | +| `commenter` 또는 확인된 SSO subject | `author_id` | identity resolver 결과만 사용 | +| 원본 테넌트 | `author_tenant_id` | 게시글/사용자 기준으로 확정 | +| `user_name` | `author_name` | 없으면 SSO profile로 보완 | +| `content` | `content` | 원문 줄바꿈 및 HTML 보존 정책 적용 | +| `created_at`, `updated_at` | 동일 대상 시간 필드 | 원본 시간 보존 | +| 댓글 이미지 존재 여부 | `attachments.comment_id` 연결 | 댓글 생성 후 이미지 이관 | + +EGBIM의 `qa_comments.commenter`는 BARON-SSO 기본 로그인 이메일이 아니라 EGBIM에서 사용하던 보조 이메일 ID일 수 있다. 따라서 댓글 이관 시 아래 규칙을 적용한다. + +- [ ] `commenter`를 원본 식별자(alias)로 보존하고, 대상 `author_id`에는 BARON-SSO의 `sso_subject`만 저장한다. +- [ ] 보조 이메일 alias를 BARON-SSO `profile.secondary_emails`와 대조하여 기본 로그인 이메일(`profile.email`), subject, tenant, 이름, 부서, 전화번호를 함께 해석한다. +- [ ] 관리자 후보 목록과 일치하고 해당 사용자의 BARON-SSO 계정이 확인되면 `ticket_comments.is_internal = true`, `comment_type = 'ADMIN'`으로 저장한다. +- [ ] 일반 사용자 댓글은 `is_internal = false`, `comment_type = 'COMMENT'`으로 저장한다. +- [ ] 보조 이메일과 SSO 계정이 매핑되지 않으면 관리자 권한을 추정하지 않고 `UNRESOLVED_IDENTITY` 예외 목록에 기록한다. +- [ ] `user_name`은 표시용 원본값으로 우선 보존하되, 검증된 SSO profile 이름이 있으면 별도 매핑 필드로 함께 저장한다. + +현재 확정된 EGBIM 관리자 후보 보조 이메일은 다음과 같다. + +```text +cjy627@hanmaceng.co.kr,b23072@hanmaceng.co.kr,kjy0426@hanmaceng.co.kr,b21367@hanmaceng.co.kr,rmsgud1202@hanmaceng.co.kr +``` + +이 목록은 과거 EGBIM 댓글의 관리자 여부를 판정하기 위한 원본 alias 목록이다. 실제 현재 관리자 권한은 `baron_support.support_role_assignments`에 별도로 등록된 역할을 기준으로 하며, 이 목록만으로 새 관리자 권한을 자동 부여하지 않는다. + +### 4.3 첨부파일 + +| EGBIM 원본 | Secretary 대상 | 매핑 규칙 | +| --- | --- | --- | +| `qa_attachments` | `attachments` | `ticket_id` 연결, `comment_id = NULL` | +| `qa_attachments.post_id` | 게시글 매핑의 대상 ticket ID | 게시글 매핑 필수 | +| `ori_name` | `original_file_name` | 사용자 표시용 원본명 | +| `save_path` | `storage_key` 변환 입력 | 절대 경로를 정규화한 뒤 복사 | +| `file_size` | `file_size` | 0 또는 NULL이면 실제 파일 크기로 재계산 | +| `uploaded_at` | `created_at` | 원본 업로드 시간 보존 | + +### 4.4 댓글 이미지와 썸네일 + +`qa_comment_images`는 다음 규칙으로 `attachments`에 적재한다. + +- [ ] `comment_id`로 `ticket_comments` 대상 ID를 조회한다. +- [ ] 원본 이미지 파일을 `attachments.storage_key`에 연결한다. +- [ ] `file_name`을 `original_file_name`으로 저장하고 MIME type·확장자를 실제 파일에서 확인한다. +- [ ] `file_size = 0`인 레코드는 파일의 실제 크기를 다시 계산한다. +- [ ] `file_path`와 `thumb_path`의 파일 존재 여부를 각각 검사한다. +- [ ] 대상 모델에는 현재 `thumb_path` 컬럼이 없으므로, 기본안은 원본 이미지를 저장하고 대상 서비스에서 썸네일을 재생성하는 방식으로 한다. +- [ ] 원본 썸네일을 반드시 보존해야 하면 `attachments`에 썸네일 storage key를 추가하는 별도 Alembic migration을 먼저 설계한다. + +원본 경로에는 `/egbim/uploads/comment/...`, `/uploads/comment/...` 형식이 섞여 있으므로 문자열만 일괄 치환하지 않는다. 실제 EGBIM 서버의 업로드 루트를 확보하고 파일별 정규화·복사·검증을 수행한다. + +### 4.5 관리자 댓글·보조 이메일 매핑 + +EGBIM 화면에서 사용하는 로그인 ID와 플랫폼 로그인 이메일이 다를 수 있다. EGBIM의 관리자 계정은 BARON-SSO 프로필에서 다음처럼 해석한다. + +| 구분 | 값/출처 | 대상 저장 | +| --- | --- | --- | +| EGBIM 관리자 식별자 | `profile.secondary_emails`에 등록된 보조 이메일 | 원본 alias 및 이관 감사 정보 | +| 플랫폼 로그인 ID | BARON-SSO `profile.email` | 사용자 표시·로그인 계정 | +| 사용자 고유 식별자 | BARON-SSO `sub` | `author_id`, `support_users.sso_subject` | +| 테넌트 | BARON-SSO `tenant_id` | `author_tenant_id` | +| 관리자 권한 | `baron_support.support_role_assignments` | 현재 운영 권한 | +| 관리자 댓글 분류 | 관리자 alias와 댓글 `commenter` 일치 | `is_internal=true`, `comment_type=ADMIN` | + +Gitea의 `ADMIN_CANDIDATE_EMAILS`에는 아래 값을 쉼표로 등록한다. + +```text +cjy627@hanmaceng.co.kr,b23072@hanmaceng.co.kr,kjy0426@hanmaceng.co.kr,b21367@hanmaceng.co.kr,rmsgud1202@hanmaceng.co.kr +``` + +이 변수의 값은 EGBIM 관리자 보조 이메일 alias 목록이며, BARON-SSO 기본 로그인 이메일 목록이 아니다. 실제 이관 전에 각 alias가 어떤 SSO subject·기본 이메일·tenant로 해석되는지 매핑 CSV를 생성하고, 미매핑 alias는 이관을 중단하거나 승인된 예외로 분리한다. 관리자 등록은 운영자가 관리자 권한 설정 화면에서 `baron_support`에 직접 수행한다. + +## 5. 사용자·권한·식별자 처리 + +EGBIM SQL에는 게시글·댓글 작성자 정보가 `login_id`, `user_id`, `user_name`, 이메일성 값 등 레거시 필드로 들어 있다. 대상 권한은 BARON-SSO의 `sso_subject`와 `tenant_id`를 기준으로 해야 한다. + +작업 목록: + +- [ ] EGBIM 사용자 식별자와 BARON-SSO profile의 `sso_subject` 매핑 파일 확보 +- [ ] BARON-SSO `profile`/`email` scope를 확인하고 `secondary_emails`를 포함한 profile 조회 결과를 확보 +- [ ] `EGBIM secondary email alias -> sso_subject, tenant_id, 기본 email, name, department, phone` 매핑 파일 생성 +- [ ] 사용자별 `tenant_id`, 이메일, 이름, 전화번호, 부서 조회 +- [ ] 매핑되지 않는 작성자는 임시 사용자로 자동 승격하지 않고 `UNRESOLVED_IDENTITY` 상태로 분리 +- [ ] `support_users`에 신규 사용자 생성이 필요한지, 기존 SSO 로그인 시 자동 동기화로 충분한지 결정 +- [ ] EGBIM workspace의 기본 역할을 `END_USER`로 설정 +- [ ] 관리자·프로젝트 관리자는 운영자가 BARON-SSO 기본 이메일로 확인한 뒤 `baron_support.support_role_assignments`에 별도 등록 +- [ ] 관리자 후보 alias와 `qa_comments.commenter`가 일치하는 댓글을 `is_internal=true`, `comment_type=ADMIN`으로 변환 +- [ ] 이관 데이터의 작성자와 댓글 작성자가 현재 로그인 사용자로 잘못 치환되지 않는지 검증 + +## 6. 상태·공개범위 매핑 + +원본 게시글 상태는 `new`, `review`, `deep`, `patch`, `done`이다. 현재 Secretary 표준 상태 코드와 일대일 대응하지 않으므로 아래를 초기 제안으로 사용하되, 실행 전 `support_status_codes`와 운영 화면 표시명을 확정한다. + +| EGBIM 상태 | Secretary 제안 | 비고 | +| --- | --- | --- | +| `new` | `RECEIVED` | 접수 상태 | +| `review` | `IN_REVIEW` | 대상 상태 코드 존재 여부 확인 | +| `deep` | `DETAILED_REVIEW` | 정밀검토중 | +| `patch` | `IN_PROGRESS` | 수정 진행 단계; 원본 상태는 `extra_fields` 보존 | +| `done` | `RESOLVED` | 종료/해결 상태 | + +- [ ] `IN_REVIEW`와 `DETAILED_REVIEW`의 표시명(검토중·정밀검토중)과 순서를 확인 +- [ ] `deep`은 `DETAILED_REVIEW`, `patch`는 `IN_PROGRESS`로 분리하고 원본 상태를 `extra_fields.legacy_status`에 저장 +- [ ] EGBIM `category` 값 `error/improvement/general`을 각각 `ERROR_QNA/IMPROVEMENT_QNA/GENERAL_QNA`로 변환하고 화면에는 오류문의·개선문의·일반문의로 표시 +- [ ] `is_read_admin`은 상태값으로 변환하지 않고 운영용 레거시 플래그로 보존 +- [ ] `is_secret`, `is_private`, `is_internal`의 우선순위와 조회 권한 테스트 +- [ ] `support_ticket_secrets.sql` 실행 후 작성자/관리자/무권한 사용자별 비밀글 조회 테스트 + +## 7. 마이그레이션 추적 및 멱등성 + +기존 V2 설계의 `migration_batches`, `migration_mappings`, `abc_feedback_mappings`를 활용한다. + +### 7.1 필수 매핑 종류 + +| `source_entity_type` | 원본 ID | 대상 | +| --- | --- | --- | +| `POST` | `qa_posts.post_id` | `support_tickets.id`, 선택적으로 ABC feedback ID | +| `COMMENT` | `qa_comments.comment_id` | `ticket_comments.id` | +| `POST_ATTACHMENT` | `qa_attachments.id` | `attachments.id` | +| `COMMENT_IMAGE` | `qa_comment_images.id` | `attachments.id` + comment ID | + +각 매핑은 `source_system = EGBIM_QA`와 함께 유일해야 한다. 재실행 시 이미 `SYNCED`인 원본은 건너뛰고, `FAILED`만 오류 원인과 함께 재처리한다. + +### 7.2 배치 단계 + +1. `PRECHECK`: 원본 행 수, 파일 존재, 사용자 식별자, 상태 코드, 중복 여부를 검사한다. +2. `WORKSPACE_READY`: EGBIM workspace/channel/ABC mapping을 확인한다. +3. `IDENTITY_READY`: 작성자와 댓글 작성자의 SSO 매핑을 확정한다. +4. `TICKETS_IMPORTED`: 게시글을 `support_tickets`로 적재한다. +5. `COMMENTS_IMPORTED`: 게시글 매핑을 이용해 댓글을 적재한다. +6. `FILES_IMPORTED`: 게시글 첨부와 댓글 이미지를 복사하고 `attachments`를 적재한다. +7. `ABC_LINKED`: 기존 ABC 레코드 연결 또는 신규 ABC 생성 결과를 기록한다. +8. `VALIDATED`: 건수·관계·권한·파일 열람 검증을 통과한다. + +ABC 레코드를 신규 생성할지는 별도 결정한다. 기존 ABC에 이미 같은 데이터가 있다면 중복 생성하지 않고 `abc_feedback_mappings`를 연결하는 것을 우선한다. + +## 8. 파일 이관 작업 + +- [ ] EGBIM 원본 서버에서 게시글 첨부와 댓글 이미지의 실제 업로드 디렉터리 확보 +- [ ] `save_path`, `file_path`, `thumb_path`가 가리키는 파일의 존재 여부 수집 +- [ ] 파일별 SHA-256, MIME type, 실제 크기 계산 +- [ ] 대상 스테이징 업로드 루트와 보존 정책 확인 +- [ ] 파일명 충돌 방지를 위해 대상 `storage_key`를 UUID 기반으로 생성 +- [ ] `original_file_name`은 표시용으로만 사용하고 경로에는 사용하지 않음 +- [ ] 경로 탈출(`..`), 절대 경로, 심볼릭 링크를 차단 +- [ ] 복사 후 원본-대상 checksum과 브라우저 다운로드/썸네일 표시를 샘플 검증 +- [ ] 파일 복사 실패는 티켓·댓글 이관 실패와 분리해 재처리 가능하게 기록 + +현재 스테이징 Secretary API는 컨테이너 내부 업로드 루트를 `/app/uploads`로 사용한다. 실제 호스트 경로와 EGBIM 전용 하위 디렉터리는 배포 설정 확인 후 확정한다. + +## 9. ABC User Feedback 연계 결정 + +V2 설계상 ABC는 원본 게시글 저장소이고 Secretary는 권한·업무·연동 제어 계층이다. 따라서 다음 두 가지 중 하나를 실행 전에 선택한다. + +### 안 A: ABC와 Secretary 동시 이관 + +- 게시글마다 ABC feedback을 생성한다. +- 반환된 ABC feedback ID를 `abc_feedback_mappings`에 저장한다. +- 댓글·첨부도 ABC API 지원 범위에 맞춰 후속 생성한다. +- 중복과 API 실패 보상 처리가 필요하다. + +### 안 B: Secretary 우선 이관 후 ABC 연결 + +- 우선 `baron_support`에 원본과 첨부를 이관한다. +- 기존 ABC 레코드가 확인되는 건만 mapping한다. +- 신규 ABC 생성은 별도 동기화 배치로 실행한다. +- 초기 데이터 보존과 재검증에는 더 안전하다. + +초기 스테이징 검증은 안 B를 권장한다. 데이터 관계와 권한을 먼저 검증한 뒤, ABC 중복 여부를 확인하고 신규 ABC 생성을 진행한다. + +## 10. 실행 전 백업 및 드라이런 + +- [ ] 스테이징 `baron_support` 전체 백업 +- [ ] 스테이징 `userfeedback` 전체 백업 또는 ABC 대상 테이블 백업 +- [ ] 현재 `/home/user/baron_qa/uploads` 파일 목록 및 용량 백업 +- [ ] `egbim_qa.sql` 원본 해시 기록 +- [ ] 대상 workspace/channel/상태 코드/사용자 매핑의 사전 검증 +- [ ] 실제 INSERT 없이 변환 결과와 오류 목록만 생성하는 드라이런 실행 +- [ ] 드라이런 결과의 expected count와 실제 count 비교 +- [ ] 실패 건만 재실행하는 멱등성 테스트 + +운영 명령은 별도 이관 스크립트가 확정된 뒤 작성한다. 현재는 SQL 파일을 직접 `mysql < egbim_qa.sql` 형태로 실행하지 않는다. + +## 11. 검증 및 완료 기준 + +- [ ] 게시글 427건, 댓글 677건, 게시글 첨부 194건, 댓글 이미지 42건의 실제 원본 건수 재확인 +- [ ] 모든 게시글이 유효한 `support_tickets`와 매핑됨 +- [ ] 모든 댓글이 올바른 ticket에 연결되고 고아 댓글이 없음 +- [ ] 모든 게시글 첨부가 올바른 ticket에 연결됨 +- [ ] 모든 댓글 이미지가 올바른 comment와 ticket에 연결됨 +- [ ] 작성자·댓글 작성자의 SSO subject/tenant 매핑 누락 목록이 0건이거나 승인된 예외 목록과 일치함 +- [ ] 관리자 후보 alias와 일치하는 댓글 수 및 관리자 댓글 ID 목록이 예상 결과와 일치함 +- [ ] 관리자 후보가 아닌 댓글이 관리자 댓글로 분류되지 않음 +- [ ] 상태별 건수가 원본과 일치하고 `legacy_status`가 보존됨 +- [ ] 비밀글·내부글 공개 범위가 일반 사용자에게 노출되지 않음 +- [ ] 관리자 콘솔에서 EGBIM workspace의 목록·상세·댓글·첨부를 조회 가능 +- [ ] 일반 사용자는 본인에게 허용된 범위의 피드백만 조회 가능 +- [ ] 원본 파일과 댓글 이미지 썸네일이 브라우저에서 정상 표시됨 +- [ ] ABC 연결 대상의 링크와 ID가 `abc_feedback_mappings`에서 조회됨 +- [ ] 배치 재실행 시 중복 ticket/comment/attachment가 생성되지 않음 +- [ ] 실패·고아·미매핑 파일 보고서가 생성됨 + +## 12. 실행 전 확정이 필요한 항목 + +1. EGBIM 원본 업로드 파일이 실제로 위치한 서버 디렉터리 +2. 스테이징의 EGBIM workspace ID, 기본 channel ID, ABC project/channel ID +3. EGBIM 사용자 식별자와 BARON-SSO `sso_subject` 매핑 제공 방식 +4. `profile.secondary_emails` 조회 권한 및 보조 이메일 alias 매핑 결과 +5. Gitea `ADMIN_CANDIDATE_EMAILS`에 등록할 관리자 alias 5개와 실제 관리자 role assignment 대상 +6. 관리자 댓글의 `is_internal/comment_type` 변환 및 일반 사용자 댓글의 공개 범위 +7. `review` 상태를 위한 `IN_REVIEW`, `deep` 상태를 위한 `DETAILED_REVIEW` 표준 코드 확인 +8. `qa_comment_images.thumb_path`를 대상에서 보존할지 재생성할지 +9. ABC 레코드를 신규 생성할지, 기존 ABC만 연결할지 +10. 개인정보 필드(phone, company, department)의 보존·마스킹 정책 +11. 이관 중 원본 EGBIM을 읽기 전용으로 전환할 시점과 최종 증분 이관 여부 + +## 13. 완료 후 산출물 + +- 이관 배치 ID 및 실행 로그 +- 원본-대상 ID 매핑 CSV/DB 조회 결과 +- 사용자 식별자 미매핑 보고서 +- 첨부파일 누락·checksum 불일치 보고서 +- 상태·공개범위 변환 결과 보고서 +- ABC 중복/연결/생성 결과 보고서 +- 롤백용 백업 위치와 복구 절차 diff --git a/docs/관리페이지 md 파일/integrated-management-feedback-tasks.md b/docs/관리페이지 md 파일/integrated-management-feedback-tasks.md new file mode 100644 index 0000000..374c83f --- /dev/null +++ b/docs/관리페이지 md 파일/integrated-management-feedback-tasks.md @@ -0,0 +1,108 @@ +# 통합관리 피드백 반영 작업 목록 + +- 정리일: 2026-09-08 +- 대상: 통합관리 페이지(피드백/이슈), 피드백 상세 페이지 +- 상태 표기: `[ ]` 미착수, `[?]` 정책 또는 화면 기준 확인 필요 + +## 1. 통합관리 페이지 + +### 1.1 상단 구조 및 대시보드 + +- [x] `피드백 처리` / `이슈 처리` 탭과 일자 선택 영역을 페이지 최상단으로 유지한다. +- [x] 상단 대시보드 데이터 박스의 크기를 축소한다. +- [x] 대시보드에서 `담당자 미지정`, `피드백 상태 처리 필요` 카드를 제외한다. +- [x] 필터 영역의 `필터` 텍스트 라벨을 제거한다. +- [x] 검색창을 우측 정렬하고, 크기·강조 스타일을 조정해 주요 조작 요소로 보이게 한다. +- [x] `업데이트일 기준` 드롭다운과 별도 일자 선택 UI를 제거한다. + +### 1.2 피드백 목록·필터·열 + +- [x] 피드백 상태를 `전체` 기본값을 포함한 다중 선택 필터로 제공한다. +- [x] 목록의 `처리항목` 열을 제거한다. +- [x] 목록에 `중요도` 열을 추가한다. +- [x] 목록 우측의 건수 표시를 제거한다. + +## 2. 피드백 상세 페이지 + +### 2.1 상단 정보와 레이아웃 + +- [x] 작성자 정보를 상세 상단으로 이동한다. +- [x] `Feedback 상세` 제목 옆에 작성자 정보를 한 줄로 배치한다. +- [x] 피드백 제목과 내용을 시각적으로 더 강조한다. +- [?] 작성자 정보에 UUID, 소속, 직책을 표시한다. +- [x] `내부 메모`를 좌측으로 이동해 첨부파일 아래에 배치한다. +- [x] 이슈 상태, 피드백 상태, 담당자 영역을 한 줄로 배치한다. +- [x] 중요도를 상세 화면에 표시한다. + +### 2.2 상태·첨부파일 표시 + +- [x] 피드백 상태 값에 상태별 색상을 적용한다. +- [x] 이슈 상태의 `연결된 이슈 없음` 텍스트를 제거한다. +- [x] 첨부파일이 이미지이면 썸네일 미리보기를 표시한다. +- [?] “피드백 내부 상태”의 상태값·전환 규칙·화면 노출 범위는 후속 정책으로 정한다. + +## 3. 삭제 권한 및 이력 보존 + +### 3.1 피드백 전체 삭제 + +- [x] 피드백 전체 삭제 권한을 시스템 관리자에게만 부여한다. +- [x] 물리 삭제 대신 삭제 상태를 기록한다. +- [x] 삭제자 식별자와 삭제 시각을 DB 이력에 저장한다. 삭제 사유는 입력 정책 확정 후 추가한다. +- [x] 일반 목록·상세에서는 삭제된 피드백을 기본적으로 제외한다. + +### 3.2 내부 메모·댓글 삭제 + +- [x] 내부 메모 삭제 시 물리 삭제 대신 삭제 상태를 기록한다. +- [x] 관리자 댓글 삭제 시 물리 삭제 대신 삭제 상태를 기록한다. +- [x] 사용자 댓글 삭제 시 물리 삭제 대신 삭제 상태를 기록한다. +- [x] 각 삭제 기록에 삭제자 식별자와 삭제 시각을 저장한다. +- [?] 삭제된 메모·댓글을 관리자에게 표시할지, 완전히 숨길지와 복구 기능 제공 여부를 정한다. + +### 3.3 이슈 상세·삭제 정책 + +- [x] 이슈 상태를 별도 편집 모드 없이 상세 화면에서 직접 변경한다. +- [x] 이슈 내부 메모를 작성자·작성일별 목록, 개별 수정·삭제 방식으로 전환한다. +- [x] 기존 단일 이슈 내부 메모 값은 신규 이력 테이블로 이전한다. +- [x] 이슈 내부 메모 삭제 시 소프트 삭제, 삭제자·삭제 시각 기록을 적용한다. +- [x] 이슈 전체 삭제를 시스템 관리자 전용 소프트 삭제로 전환한다. + +## 4. 데이터·API 점검 항목 + +- [?] UUID·소속·직책이 현재 SSO/ABC 사용자 원본에서 제공되는지 확인한다. +- [x] 중요도의 저장 위치와 기존 피드백 데이터의 기본값을 확인한다. +- [x] 상태 다중 선택을 통합관리 화면의 로컬 목록 필터에 반영한다. +- [x] 소프트 삭제 필드와 이력 조회 권한을 API·DB migration에 반영한다. +- [ ] 외부 작성페이지 및 통합관리의 API 연동이 삭제 상태와 상세 표시 변경 후에도 동일하게 동작하는지 검증한다. + +## 확인이 필요한 사항 + +1. 상단 대시보드에서 실제로 제거할 카드가 무엇인지 확정이 필요하다. +2. 작성자 `UUID`는 SSO의 `sub`(SSO subject)를 표시하면 되는지, 아니면 ABC `users.id`를 뜻하는지 확인이 필요하다. +3. 작성자 `직책` 데이터의 원본 필드·API 제공 여부를 확인해야 한다. 현재 확인된 SSO profile에는 이름·이메일·전화번호·소속 등이 있었지만 직책은 별도 항목일 수 있다. +4. 삭제된 내부 메모/댓글을 시스템 관리자에게라도 이력으로 보여줄지, DB에만 남기고 UI에서는 숨길지 결정이 필요하다. + +## 이번 반영의 저장 방식 + +- 피드백은 ABC `feedbacks.deleted_at`의 TypeORM soft delete를 사용한다. 삭제 이벤트는 `histories`에 `SoftDelete` 액션, 삭제 시각, 요청 사용자 ID를 남긴다. +- 내부 메모와 관리자/사용자 댓글은 `ticket_comments.deleted_at`을 유지하고, 신규 migration `0019_comment_deletion_audit`에서 `deleted_by_id`, `deleted_by_tenant_id`를 추가한다. +- 삭제된 메모·댓글을 화면에 보여줄지 여부는 보류되어 있으므로 기존처럼 일반 조회에서는 제외한다. +- 이슈 내부 메모는 `issue_internal_memos`에 별도 이력으로 저장하며, `deleted_at`, `deleted_by_user_id`, `deleted_by_name`을 유지한다. 이슈 자체도 피드백과 동일하게 시스템 관리자만 소프트 삭제할 수 있다. + +## SSO 작성자 정보 전달 계약 + +작성 페이지는 SSO ID 토큰을 그대로 전송하지 않는다. 다만 8864의 실제 로그인 세션은 버전에 따라 ID 토큰을 `sessionStorage` 또는 `jwt` 쿠키에 보관하므로, 두 위치에서 토큰을 읽어 작성자 표시용 claims만 평탄화해 전송한다. + +로그인 API도 같은 최소 조직 정보를 자체 서명 access token에 넣고, Secretary가 티켓 생성 시 이를 `extra_fields`와 ABC 메타데이터에 평탄화해 저장한다. 이 서버 경로는 브라우저 정보가 누락된 세션의 보완 수단이다. 원문 토큰·세션 digest·토큰 만료값은 어느 요청이나 피드백 데이터에 저장하지 않는다. + +| 저장 필드 | SSO ID 토큰 기준 값 | +| ------------------------ | ------------------------------------------- | +| `requester_id` | `sub` (SSO UUID) | +| `requester_uuid` | `sub` (호환용 중복 보관) | +| `requester_tenant_id` | `tenant_id` | +| `requester_name` | `profile.name` 또는 `name` | +| `requester_email` | `profile.email` 또는 `email` | +| `requester_phone_number` | `profile.phones[0]` | +| `requester_department` | 기본 테넌트(`tenants[tenant_id]`)의 `name` | +| `requester_affiliation` | 상위 `COMPANY` 테넌트의 `name` | +| `requester_position` | 기본 테넌트의 `position` 또는 `grade` | +| `requester_grade` | 기본 테넌트의 `grade` | diff --git a/docs/관리페이지 md 파일/local-userfeedback-erd.md b/docs/관리페이지 md 파일/local-userfeedback-erd.md new file mode 100644 index 0000000..7a4956b --- /dev/null +++ b/docs/관리페이지 md 파일/local-userfeedback-erd.md @@ -0,0 +1,686 @@ +# 로컬 `userfeedback` DB ERD + +- 추출 기준: 2026-09-17 +- 대상: `abc-user-feedback-mysql-1 / userfeedback` +- 테이블: 33개 +- UUID 저장 형식: `BINARY(16)` (애플리케이션 경계에서는 UUID v7 문자열) +- 관계 기준: MySQL `information_schema.KEY_COLUMN_USAGE`에 등록된 물리 FK + +## 1. 테넌트·사용자·프로젝트·채널 + +```mermaid +%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%% +erDiagram + TENANT o|--o{ PROJECTS : "tenant_id" + PROJECTS o|--o{ CHANNELS : "project_id" + PROJECTS o|--o{ ROLES : "project_id" + USERS o|--o{ MEMBERS : "user_id" + ROLES o|--o{ MEMBERS : "role_id" + PROJECTS ||--o{ CATEGORIES : "project_id" + CHANNELS o|--o{ FIELDS : "channel_id" + FIELDS o|--o{ OPTIONS : "field_id" + PROJECTS o|--o{ API_KEYS : "project_id" + PROJECTS o|--o| ISSUE_TRACKERS : "project_id" + PROJECTS o|--o{ WEBHOOKS : "project_id" + WEBHOOKS o|--o{ EVENTS : "webhook_id" + EVENTS ||--o{ EVENTS_CHANNELS_CHANNELS : "events_id" + CHANNELS ||--o{ EVENTS_CHANNELS_CHANNELS : "channels_id" + + TENANT { + uuid id PK + string site_name + string description + bool use_email + text allow_domains + bool use_o_auth + json oauth_config + datetime created_at + datetime updated_at + datetime deleted_at + } + + USERS { + uuid id PK + string email "UK with sign_up_method" + string name + string department + enum state + string hash_password + enum type + enum sign_up_method + string oauth_subject "UK with tenant and method" + string oauth_tenant_id + json secondary_emails + string phone_number + datetime created_at + datetime updated_at + datetime deleted_at + } + + PROJECTS { + uuid id PK + uuid tenant_id FK + string name UK + string description + string timezone + json issue_admin_user_ids + bool show_feedback_comment_author_name + datetime created_at + datetime updated_at + datetime deleted_at + } + + CHANNELS { + uuid id PK + uuid project_id FK + string name "UK with project_id" + string description + json image_config + int feedback_search_max_days + datetime created_at + datetime updated_at + datetime deleted_at + } + + ROLES { + uuid id PK + uuid project_id FK + string name + text permissions + datetime created_at + datetime updated_at + datetime deleted_at + } + + MEMBERS { + uuid id PK + uuid role_id FK "UK with user_id" + uuid user_id FK "UK with role_id" + datetime created_at + datetime updated_at + datetime deleted_at + } + + CATEGORIES { + uuid id PK + uuid project_id FK + string name "UK with project_id" + datetime created_at + datetime updated_at + datetime deleted_at + } + + FIELDS { + uuid id PK + uuid channel_id FK + uuid ai_field_template_id FK + string name "UK with channel_id" + string key "UK with channel_id" + string description + enum format + enum status + enum property + int order + json ai_field_target_keys + bool ai_field_auto_processing + datetime created_at + datetime updated_at + datetime deleted_at + } + + OPTIONS { + uuid id PK + uuid field_id FK + string name + string key + datetime created_at + datetime updated_at + datetime deleted_at + } + + API_KEYS { + uuid id PK + uuid project_id FK + string value UK + datetime created_at + datetime updated_at + datetime deleted_at + } + + ISSUE_TRACKERS { + uuid id PK + uuid project_id FK,UK + json data + datetime created_at + datetime updated_at + datetime deleted_at + } + + WEBHOOKS { + uuid id PK + uuid project_id FK + string name + string url + string token + enum status + datetime created_at + datetime updated_at + datetime deleted_at + } + + EVENTS { + uuid id PK + uuid webhook_id FK + enum status + enum type + datetime created_at + datetime updated_at + datetime deleted_at + } + + EVENTS_CHANNELS_CHANNELS { + uuid events_id PK,FK + uuid channels_id PK,FK + } +``` + +## 2. 피드백·댓글·이슈 + +```mermaid +%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%% +erDiagram + CHANNELS o|--o{ FEEDBACKS : "channel_id" + FEEDBACKS ||--o{ FEEDBACK_COMMENTS : "feedback_id" + FEEDBACK_COMMENTS ||--o{ FEEDBACK_COMMENT_ATTACHMENTS : "comment_id" + PROJECTS o|--o{ ISSUES : "project_id" + CATEGORIES o|--o{ ISSUES : "category_id" + FEEDBACKS ||--o{ FEEDBACKS_ISSUES_ISSUES : "feedbacks_id" + ISSUES ||--o{ FEEDBACKS_ISSUES_ISSUES : "issues_id" + ISSUES ||--o{ ISSUE_INTERNAL_MEMOS : "issue_id" + CHANNELS o|--o{ FEEDBACK_STATISTICS : "channel_id" + PROJECTS o|--o{ ISSUE_STATISTICS : "project_id" + ISSUES o|--o{ FEEDBACK_ISSUE_STATISTICS : "issue_id" + + FEEDBACKS { + uuid id PK + uuid channel_id FK + json data + datetime admin_first_read_at + uuid admin_first_read_by "logical users.id" + string source_namespace "UK with source_record_id" + string source_record_id + datetime created_at + datetime updated_at + datetime deleted_at + } + + FEEDBACK_COMMENTS { + uuid id PK + uuid feedback_id FK + string author_id + string author_tenant_id + string author_name + string author_type + text content + bool is_internal + string comment_type + string idempotency_key "UK with feedback_id" + string source_namespace "UK with feedback_id and record" + string source_record_id + datetime created_at + datetime updated_at + datetime deleted_at + } + + FEEDBACK_COMMENT_ATTACHMENTS { + uuid id PK + uuid comment_id FK + string original_file_name + string storage_key "UK with storage_bucket" + string storage_bucket + string mime_type + bigint file_size + string source_namespace "UK with source_record_id" + string source_record_id + datetime created_at + datetime updated_at + datetime deleted_at + } + + ISSUES { + uuid id PK + uuid project_id FK + uuid category_id FK + uuid issue_admin_user_id "logical users.id" + uuid admin_first_read_by "logical users.id" + uuid hold_set_by "logical users.id" + uuid hold_released_by "logical users.id" + string name "UK with project_id" + string description + enum status + string external_issue_id "UK with provider and project" + string external_issue_provider + string external_issue_url + string external_issue_status + string external_issue_sync_status + text external_issue_sync_error + datetime external_issue_synced_at + int feedback_count + text internal_memo + datetime admin_first_read_at + text hold_reason + datetime hold_set_at + datetime hold_released_at + string hold_resume_status + datetime completed_at + datetime created_at + datetime updated_at + datetime deleted_at + } + + FEEDBACKS_ISSUES_ISSUES { + uuid feedbacks_id PK,FK + uuid issues_id PK,FK + } + + ISSUE_INTERNAL_MEMOS { + uuid id PK + uuid issue_id FK + uuid author_id "logical users.id" + uuid deleted_by_user_id "logical users.id" + string author_name + text content + string deleted_by_name + datetime created_at + datetime updated_at + datetime deleted_at + } + + FEEDBACK_STATISTICS { + uuid id PK + uuid channel_id FK + date date "UK with channel_id" + int count + } + + ISSUE_STATISTICS { + uuid id PK + uuid project_id FK + date date "UK with project_id" + int count + } + + FEEDBACK_ISSUE_STATISTICS { + uuid id PK + uuid issue_id FK + date date "UK with issue_id" + int feedback_count + } + + CHANNELS { + uuid id PK + } + + PROJECTS { + uuid id PK + } + + CATEGORIES { + uuid id PK + } +``` + +## 3. AI·이력·멱등성·운영 + +```mermaid +%%{init: {"themeVariables": {"fontSize": "20px"}, "er": {"fontSize": 20, "minEntityWidth": 220, "minEntityHeight": 120, "entityPadding": 18, "diagramPadding": 24}}}%% +erDiagram + PROJECTS ||--o{ AI_FIELD_TEMPLATES : "project_id" + PROJECTS ||--o| AI_INTEGRATIONS : "project_id" + PROJECTS ||--o{ AI_USAGES : "project_id" + CHANNELS ||--o{ AI_ISSUE_TEMPLATES : "channel_id" + AI_FIELD_TEMPLATES o|--o{ FIELDS : "ai_field_template_id" + + AI_FIELD_TEMPLATES { + uuid id PK + uuid project_id FK + string title + text prompt + string model + float temperature + datetime created_at + datetime updated_at + datetime deleted_at + } + + AI_INTEGRATIONS { + uuid id PK + uuid project_id FK,UK + enum provider + string api_key + string endpoint_url + text system_prompt + int token_threshold + float notification_threshold + datetime created_at + datetime updated_at + datetime deleted_at + } + + AI_ISSUE_TEMPLATES { + uuid id PK + uuid channel_id FK + json target_field_keys + text prompt + bool is_enabled + string model + float temperature + int data_reference_amount + datetime created_at + datetime updated_at + datetime deleted_at + } + + AI_USAGES { + uuid id PK + uuid project_id FK + int year + int month + int day + enum category + enum provider + int used_tokens + datetime created_at + datetime updated_at + datetime deleted_at + } + + HISTORIES { + uuid id PK + uuid user_id "logical users.id" + enum entity_name + string entity_id + enum action + json entity + datetime created_at + datetime updated_at + datetime deleted_at + } + + IDEMPOTENCY_RECORDS { + uuid id PK + string consumer "UK with idempotency_key" + string idempotency_key + string request_hash + string resource_type + uuid resource_id "logical polymorphic id" + datetime expires_at + datetime created_at + datetime updated_at + datetime deleted_at + } + + NOTIFICATION_DELIVERIES { + uuid id PK + string idempotency_key UK + string event_type + string event_id + enum status + int attempts + text last_error + datetime sent_at + string target_type + string target_id + uuid project_id "logical projects.id" + uuid channel_id "logical channels.id" + uuid feedback_id "logical feedbacks.id" + datetime created_at + datetime updated_at + datetime deleted_at + } + + CODES { + uuid id PK + enum type + string key + string code + string data + bool is_verified + datetime expired_at + int try_count + datetime created_at + datetime updated_at + datetime deleted_at + } + + SCHEDULER_LOCKS { + enum lock_type PK + string server_id + datetime timestamp + } + + MIGRATIONS { + int id PK + bigint timestamp + string name + } + + PROJECTS { + uuid id PK + } + + CHANNELS { + uuid id PK + } + + FIELDS { + uuid id PK + } +``` + +## 4. 주요 물리 Unique Key + +| 테이블 | 컬럼 조합 | +| ------------------------------ | --------------------------------------------------------------------------------- | +| `projects` | `name` | +| `channels` | `name, project_id` | +| `categories` | `project_id, name` | +| `fields` | `key, channel_id`; `name, channel_id` | +| `members` | `role_id, user_id` | +| `users` | `email, sign_up_method`; `oauth_subject, oauth_tenant_id, sign_up_method` | +| `feedbacks` | `source_namespace, source_record_id` | +| `feedback_comments` | `feedback_id, idempotency_key`; `feedback_id, source_namespace, source_record_id` | +| `feedback_comment_attachments` | `storage_bucket, storage_key`; `source_namespace, source_record_id` | +| `issues` | `name, project_id`; `external_issue_provider, project_id, external_issue_id` | +| `idempotency_records` | `consumer, idempotency_key` | +| `ai_integrations` | `project_id` | +| `issue_trackers` | `project_id` | + +## 5. 물리 FK가 없는 논리 참조 + +다음 컬럼은 UUID 또는 식별자를 저장하지만 현재 MySQL FK 제약은 없다. + +- `feedbacks.admin_first_read_by → users.id` +- `issues.issue_admin_user_id → users.id` +- `issues.admin_first_read_by → users.id` +- `issues.hold_set_by → users.id` +- `issues.hold_released_by → users.id` +- `issue_internal_memos.author_id → users.id` +- `issue_internal_memos.deleted_by_user_id → users.id` +- `histories.user_id → users.id` +- `idempotency_records.resource_id`는 다형성 리소스 참조 +- `notification_deliveries.project_id/channel_id/feedback_id`는 이벤트 스냅샷 참조 + +## 6. DB 설계 이유 + +### 6.1 현재 규모에서는 단일 MySQL이 가장 단순하고 안정적이다 + +예상 데이터가 댓글과 피드백을 합쳐 연간 약 5만 건이라면, 단일 MySQL 인스턴스가 처리하기에 충분히 작은 규모다. 이 단계에서 물리적 샤딩이나 여러 DB로 데이터를 나누면 성능 이득보다 분산 트랜잭션, 데이터 정합성, 장애 복구 및 운영 복잡성이 더 커진다. + +따라서 현재 설계는 다음 원칙을 사용한다. + +- 업무 데이터의 최종 권위는 MySQL 하나로 유지한다. +- Redis는 캐시·멱등성 선점·일시적 잠금에 사용하되 최종 정합성의 기준으로 사용하지 않는다. +- 실제 CPU, 디스크 I/O, Buffer Pool, Lock wait 또는 목록 조회 p95가 허용 범위를 지속해서 넘을 때 읽기 복제본이나 기능별 분리를 검토한다. +- UUID를 사용해 향후 여러 서버에서 동시에 데이터를 생성하더라도 PK 발급을 중앙 DB의 `AUTO_INCREMENT`에 의존하지 않는다. + +즉, 분산 환경에서 사용할 수 있는 식별자와 중복 방지 구조는 미리 갖추되, 데이터 저장소 자체는 현재 규모에 맞게 단순하게 유지한 설계다. + +### 6.2 UUID v7을 모든 주요 엔티티의 PK로 사용한다 + +`tenant`, `projects`, `channels`, `feedbacks`, `issues`, 댓글과 첨부파일 등 주요 엔티티의 PK는 UUID v7이며 MySQL에는 `BINARY(16)`으로 저장한다. + +이 방식을 선택한 이유는 다음과 같다. + +- 여러 API 인스턴스가 DB 왕복 없이 충돌 가능성이 매우 낮은 ID를 직접 생성할 수 있다. +- UUID v7은 시간 순서 특성이 있어 UUID v4보다 B-Tree 인덱스의 무작위 페이지 분할과 단편화를 줄일 수 있다. +- 외부 URL과 API에서 동일 UUID를 사용하므로 내부 ID와 외부 ID를 변환하는 조회 로직이 필요 없다. +- 프로젝트나 채널을 다른 DB 또는 서비스로 이동하더라도 ID 충돌 없이 그대로 유지할 수 있다. +- MySQL에서는 문자열 `CHAR(36)` 대신 `BINARY(16)`을 사용해 PK 및 FK 인덱스 크기를 줄인다. + +UUID는 DB 관리 도구에서 깨진 문자처럼 보일 수 있지만 데이터 손상이 아니라 16바이트 바이너리를 문자로 표시해서 발생하는 현상이다. 조회할 때는 `BIN_TO_UUID(id)`를 사용해야 한다. + +### 6.3 `tenant → project → channel` 계층으로 데이터 소유 범위를 구분한다 + +데이터의 기본 소유 계층은 다음과 같다. + +```text +Tenant +└── Project + ├── Channel + │ ├── Field / Option + │ └── Feedback + ├── Role / Member + ├── Category / Issue + ├── API Key + └── Webhook / Integration +``` + +- `tenant`는 고객 또는 조직의 최상위 경계다. +- `projects`는 권한, API Key, 이슈 및 외부 연동의 관리 단위다. +- `channels`는 프로젝트 안에서 피드백 입력 양식과 피드백 데이터를 분리하는 단위다. +- 프로젝트별 역할은 `roles`, 사용자 할당은 `members`로 분리해 한 사용자가 여러 프로젝트에서 서로 다른 권한을 가질 수 있다. + +이 계층은 현재 단일 DB에서도 데이터 격리를 명확하게 하고, 향후 필요하면 `tenant_id` 또는 `project_id`를 기준으로 데이터 배치와 분할 범위를 결정할 수 있게 한다. + +### 6.4 피드백 본문은 JSON, 검색·관계 기준은 정규 컬럼으로 분리한다 + +채널마다 입력 필드가 다르므로 `feedbacks.data`는 JSON으로 저장한다. 필드 정의는 `fields`, 선택지는 `options`에서 관리한다. + +모든 입력값을 고정 컬럼으로 만들지 않은 이유는 다음과 같다. + +- 채널별 양식 변경 시 매번 DB migration을 만들 필요가 없다. +- 사용자 정의 필드와 AI 생성 필드를 동일한 구조로 처리할 수 있다. +- 기존 피드백 데이터를 유지하면서 새 필드를 추가하거나 비활성화할 수 있다. + +반면 검색과 무결성에 자주 사용하는 값은 JSON에 넣지 않고 정규 컬럼으로 둔다. + +- `id`, `channel_id`, `created_at`, `deleted_at` +- 외부 중복 방지용 `source_namespace`, `source_record_id` +- 관리자 최초 열람 정보 + +특정 JSON 필드 검색이 반복적으로 느려질 때만 Generated Column과 인덱스를 추가한다. 연간 5만 건 수준에서 사용 여부가 확인되지 않은 모든 JSON 키를 미리 인덱싱하면 쓰기 비용과 운영 복잡성만 증가하기 때문이다. + +### 6.5 댓글과 첨부파일을 별도 엔티티로 분리한다 + +댓글은 `feedback_comments`, 댓글 첨부파일은 `feedback_comment_attachments`로 분리했다. + +- 하나의 피드백에 여러 댓글을 시간순으로 저장할 수 있다. +- 공개 댓글과 내부 댓글을 `is_internal`로 구분할 수 있다. +- 첨부파일 메타데이터만 DB에 두고 실제 파일은 로컬 볼륨 또는 Object Storage에 저장할 수 있다. +- 댓글 삭제와 첨부파일 정리 정책을 피드백 본문과 독립적으로 적용할 수 있다. +- 댓글 및 첨부파일도 각각 외부 원본 식별자를 가져 재전송 중복을 막을 수 있다. + +파일 자체를 DB BLOB으로 저장하지 않는 것은 DB 백업 크기와 I/O 부하를 줄이고, 향후 R2/S3 같은 Object Storage로 전환하기 쉽게 하기 위한 선택이다. + +### 6.6 피드백과 이슈는 N:M 관계로 설계한다 + +`feedbacks_issues_issues` 연결 테이블을 두어 하나의 피드백을 여러 이슈와 연결하고, 하나의 이슈가 여러 피드백을 묶을 수 있게 했다. + +예를 들어 여러 사용자가 같은 장애를 각각 제보하면 피드백은 개별 기록으로 유지하면서 하나의 이슈로 묶어 처리할 수 있다. 연결 테이블의 복합 PK `(feedbacks_id, issues_id)`는 동일 연결이 중복 저장되는 것을 DB 차원에서 방지한다. + +이슈의 외부 연동 식별자는 다음 조합으로 유일성을 보장한다. + +```text +external_issue_provider + project_id + external_issue_id +``` + +따라서 Gitea, Jira, GitHub가 같은 숫자 이슈 ID를 사용해도 서로 충돌하지 않으며 프로젝트가 다르면 독립적으로 관리된다. + +### 6.7 중복 요청 방지는 업무 Unique Key와 멱등성 레코드를 함께 사용한다 + +네트워크 재시도나 외부 시스템의 중복 전송으로 같은 데이터가 여러 번 생성되지 않도록 두 계층으로 방어한다. + +1. 업무 원본 중복 방지 + - `source_namespace + source_record_id` + - 원본 시스템 안에서 같은 레코드를 다시 보내면 기존 데이터를 식별한다. +2. HTTP 요청 재시도 방지 + - `consumer + idempotency_key` + - 같은 소비자의 동일 요청 키는 `idempotency_records`에서 한 번만 처리한다. + +Redis가 사용 가능한 경우 짧은 선점 잠금으로 동시 요청을 빠르게 차단하지만, 최종 중복 방지는 MySQL Unique Key가 담당한다. 따라서 Redis가 재시작되거나 일시적으로 중단되어도 중복 데이터가 확정 저장되지 않는다. + +### 6.8 Soft Delete와 이력 테이블을 사용한다 + +대부분의 업무 테이블에는 `created_at`, `updated_at`, `deleted_at`이 있다. + +- 일반 삭제는 `deleted_at`을 기록하는 Soft Delete로 처리해 실수로 삭제한 데이터를 복구할 수 있다. +- FK의 `ON DELETE CASCADE`는 프로젝트나 피드백을 실제로 물리 삭제하는 명시적 정리 작업에서 하위 데이터를 함께 제거한다. +- `histories`는 엔티티 종류, 동작, 당시 데이터를 JSON으로 기록해 관리자 변경 이력을 추적한다. + +Soft Delete만 적용하면 Unique Key의 재사용 정책이 복잡해질 수 있으므로 프로젝트명, 채널명처럼 재사용 여부가 중요한 값은 운영 정책과 함께 관리해야 한다. + +### 6.9 집계 통계는 원본 테이블과 분리한다 + +대시보드가 매번 전체 피드백과 이슈를 집계하지 않도록 다음 통계 테이블을 둔다. + +- `feedback_statistics`: 채널·일자별 피드백 수 +- `issue_statistics`: 프로젝트·일자별 이슈 수 +- `feedback_issue_statistics`: 이슈·일자별 연결 피드백 수 + +원본 데이터는 `feedbacks`와 `issues`이며 통계 테이블은 재생성 가능한 파생 데이터다. `scheduler_locks`는 여러 API 인스턴스가 같은 통계 작업을 동시에 실행하는 것을 막는다. + +연간 5만 건에서는 원본 직접 집계도 가능하지만, 대시보드 요청이 늘어날 때의 반복 스캔을 줄이고 응답시간을 안정적으로 유지하기 위해 집계 구조를 분리했다. + +### 6.10 AI와 외부 연동 설정은 업무 데이터와 분리한다 + +AI 기능은 다음과 같이 역할별로 분리했다. + +- `ai_integrations`: 프로젝트별 AI 공급자와 연결 설정 +- `ai_field_templates`: AI 필드 생성 프롬프트 +- `ai_issue_templates`: 채널별 이슈 추천 설정 +- `ai_usages`: 프로젝트별 사용량 + +웹훅도 `webhooks`, `events`, `events_channels_channels`로 분리해 하나의 웹훅에 여러 이벤트와 채널을 연결할 수 있다. 설정과 실행 데이터를 분리하면 공급자 변경, 기능 비활성화, 사용량 집계가 피드백 원본 구조에 영향을 주지 않는다. + +### 6.11 일부 사용자·이벤트 참조에 물리 FK를 두지 않은 이유 + +관리자 사용자, 이력 작성자, 알림 대상처럼 삭제 이후에도 당시 값을 보존해야 하거나 여러 엔티티를 참조할 수 있는 컬럼에는 물리 FK가 없다. + +- 사용자가 삭제되어도 과거 이력과 처리 담당자 기록은 남아야 한다. +- `idempotency_records.resource_id`는 피드백, 댓글 등 여러 리소스를 가리킬 수 있어 단일 FK를 만들 수 없다. +- `notification_deliveries`는 발송 당시의 이벤트 스냅샷이므로 원본 삭제가 알림 이력을 삭제하게 만들지 않는다. + +이 선택은 보존성과 결합도 측면에서는 유리하지만 고아 참조가 생길 수 있다. 따라서 애플리케이션 검증과 정기 무결성 점검이 필요하다. 반드시 강한 정합성이 필요한 소유 관계에는 계속 물리 FK를 사용한다. + +### 6.12 인덱스는 조회 패턴과 유일성 중심으로 구성한다 + +현재 인덱스는 다음 용도에 집중한다. + +- FK 조인: `project_id`, `channel_id`, `feedback_id`, `issue_id` +- 목록 조회: 생성일, 삭제 여부, 상태 +- 이름 중복 방지: 프로젝트명, 프로젝트 내 채널·카테고리·이슈명 +- 외부 원본 중복 방지: source identity +- 외부 이슈 중복 방지: provider/project/external ID +- 요청 재시도 방지: idempotency key + +UUID v7과 `(created_at, id)` 조합은 같은 시각에 생성된 데이터도 안정적으로 정렬하고 cursor pagination의 마지막 위치를 정확하게 표현한다. 데이터가 증가한 뒤에는 추측으로 인덱스를 늘리기보다 Slow Query Log와 실제 실행 계획을 기준으로 추가한다. + +## 7. 재추출 필요 조건 + +아래 작업 후에는 이 문서를 다시 생성해야 한다. + +1. TypeORM UUID migration 적용 또는 변경 +2. Secretary Alembic migration 적용으로 `support_*` 테이블 생성 +3. FK·Unique Key·인덱스 변경 +4. 스테이징/운영 스키마와 로컬 스키마 비교 diff --git a/docs/관리페이지 md 파일/multi-project-admin-dashboard-design.md b/docs/관리페이지 md 파일/multi-project-admin-dashboard-design.md new file mode 100644 index 0000000..d75f990 --- /dev/null +++ b/docs/관리페이지 md 파일/multi-project-admin-dashboard-design.md @@ -0,0 +1,230 @@ +# 다중 프로젝트 관리자 통합 대시보드 설계 + +## 1. 목적 + +한 명의 관리자가 여러 프로젝트의 관리자로 지정된 경우, 로그인 직후 각 프로젝트의 주요 현황을 한 화면에서 확인할 수 있는 통합 대시보드를 제공한다. + +기존 프로젝트별 대시보드는 유지하고, 여러 프로젝트를 한 화면에서 확인하고 처리하기 위한 전역 관리자 작업공간을 추가한다. 이 화면은 특정 프로젝트에 종속되지 않으며 로그인 후 홈 버튼으로 언제든 접근할 수 있다. + +## 2. 기본 진입 규칙 + +로그인 완료 후 `/main`에서 현재 사용자가 관리할 수 있는 프로젝트 수를 확인한다. + +| 조건 | 로그인 후 기본 화면 | +| --- | --- | +| 관리 프로젝트 0개 | 프로젝트 없음 안내 화면 | +| 관리 프로젝트 1개 | 기존 프로젝트 대시보드 | +| 관리 프로젝트 2개 이상 | 통합 관리자 대시보드 | +| SUPER 관리자 | 전체 프로젝트 통합 대시보드 | + +관리 프로젝트는 기존 프로젝트 멤버 역할 중 `PROJECT_MANAGER` 또는 `Admin`에 해당하는 프로젝트로 정의한다. + +## 3. 라우팅 + +### 신규 통합 대시보드 + +```text +/main/overview +``` + +### 기존 프로젝트별 화면 + +```text +/main/project/[projectId]/dashboard +/main/project/[projectId]/feedback +/main/project/[projectId]/issue +/main/project/[projectId]/settings +``` + +통합 대시보드는 특정 `projectId` 없이 접근한다. 프로젝트별 상세 작업은 기존 프로젝트 라우트로 이동한다. + +## 4. 화면 구성 + +### 4.1 전체 요약 카드 + +접근 가능한 프로젝트 전체를 기준으로 다음 지표를 표시한다. 카드와 Todo 목록은 같은 조회 기간·프로젝트 범위를 사용한다. + +- 전체 피드백 건수 +- 답변 대기 건수 +- 이슈 연결률 +- 평균 처리 시간 +- 진행 중 이슈 건수 +- 완료 피드백 건수 +- 현재 작업공간에서 처리할 Todo 건수 +- 담당자 미지정 건수 +- 피드백 상태 처리 필요 건수 +- 이슈 연결 또는 이슈 상태 처리 필요 건수 + +### 4.2 하위 탭 + +전역 작업공간은 처리 대상에 따라 두 개의 하위 탭으로 나눈다. + +- **피드백 처리**: 오늘 등록 건수 등의 요약, 담당자 지정, 관리자 댓글 등록, 피드백 상태 처리, 연결 이슈 요약을 표시한다. +- **이슈 처리**: 프로젝트별 이슈를 중복 없이 묶어 Gitea 연결·상태 처리와 연결된 피드백 목록을 표시한다. + +### 4.3 관리자 Todo 작업 목록 + +전역 작업공간의 핵심은 여러 프로젝트의 피드백을 한 목록에서 처리하는 것이다. + +| 작업 | 처리 방식 | +| --- | --- | +| 프로젝트 확인 | 프로젝트별 피드백 화면 링크 제공 | +| 담당자 지정 | 해당 프로젝트의 지원 담당자 후보 조회 후 지정·해제 | +| 피드백 상태 | 기존 지원 티켓 상태 API로 상태 변경 | +| 이슈 연결 | 기존 프로젝트 이슈 검색 후 피드백과 연결·해제 | +| 이슈 상태 | 기존 6단계 이슈 상태로 변경 | +| Gitea 연결 | Gitea 검색 결과를 기존 이슈에 연결 | +| Gitea 상태 | 연결된 외부 이슈 상태 조회·새로고침 | + +피드백 상태와 이슈 상태는 서로 독립적으로 처리한다. 이슈 상태 변경은 내부 이슈 상태를 기준으로 저장하며, Gitea 연결 이슈가 있는 경우 외부 상태 동기화를 시도한다. + +### 4.4 프로젝트별 요약 테이블 + +| 항목 | 설명 | +| --- | --- | +| 프로젝트 | 프로젝트명 및 프로젝트 이동 링크 | +| 피드백 | 프로젝트의 전체 피드백 건수 | +| 답변 대기 | 답변 또는 이슈 처리가 완료되지 않은 피드백 건수 | +| 이슈 연결률 | 이슈가 연결된 피드백 비율 | +| 평균 처리 시간 | 처리 완료된 피드백의 평균 처리 시간 | +| 상태별 건수 | 피드백 상태별 건수 요약 | +| 최근 업데이트 | 해당 프로젝트의 최근 변경 시각 | + +프로젝트 행 또는 프로젝트명을 클릭하면 해당 프로젝트의 기존 대시보드로 이동한다. + +### 4.5 필터 + +다음 필터를 제공한다. + +- 기간 필터 +- 프로젝트 필터 +- 담당자 지정 여부 +- 피드백 상태 +- 이슈 연결 여부 + +현재 단계에서는 행 단위 처리를 제공한다. 추후 현재 로그인 사용자 기준의 정확한 “내 담당” 필터, 일괄 상태 변경, 댓글·내부 메모 입력을 확장한다. + +## 5. 백엔드 API 설계 + +프로젝트별 API를 프론트엔드에서 반복 호출하지 않고, 통합 집계 API를 제공한다. + +```text +GET /api/admin/dashboard/overview +``` + +### 요청 파라미터 + +```text +from +to +projectIds[] +feedbackStatus +assigned +``` + +### 응답 예시 + +```json +{ + "summary": { + "totalFeedback": 73, + "waitingReply": 16, + "issueLinkedRate": 0.58, + "averageProcessingHours": 20.4 + }, + "projects": [ + { + "projectId": 1, + "projectName": "Q&A", + "feedbackCount": 55, + "waitingReplyCount": 12, + "issueLinkedRate": 0.67, + "averageProcessingHours": 24.5, + "statusCounts": { + "INIT": 10, + "IN_PROGRESS": 20, + "DONE": 25 + } + } + ] +} +``` + +## 6. 집계 기준 + +기존 프로젝트 대시보드와 동일한 기준을 사용한다. + +- 이슈 연결률은 전체 피드백 중 이슈가 연결된 피드백의 비율로 계산한다. +- 답변 대기는 답변이 없거나 이슈 처리가 완료되지 않은 피드백 건수로 계산한다. +- 평균 처리 시간은 처리 완료된 피드백만 대상으로 계산한다. +- 여러 프로젝트의 평균 처리 시간은 프로젝트별 평균을 다시 평균내지 않고, 전체 처리 시간 합계를 전체 처리 완료 건수로 나누어 계산한다. +- 상태별 건수는 피드백 상태값 기준으로 집계한다. +- 이슈 상태와 피드백 상태는 서로 독립적으로 집계한다. + +## 7. 권한 및 보안 + +통합 API는 서버에서 사용자의 프로젝트 접근 권한을 다시 검증해야 한다. + +- 일반 관리자는 자신이 관리자로 등록된 프로젝트만 조회한다. +- `SUPER` 관리자는 전체 프로젝트를 조회할 수 있다. +- 요청한 `projectIds` 중 접근 권한이 없는 프로젝트는 결과에서 제외하거나 `403 Forbidden`으로 처리한다. +- 프론트엔드의 프로젝트 목록 제한은 UI 편의 기능일 뿐, 최종 권한 검증은 백엔드에서 수행한다. + +현재 프로젝트 멤버십 및 역할 구조를 재사용하며, 1차 구현에서는 별도 상위 프로젝트 테이블을 추가하지 않는다. + +## 8. 헤더 및 전역 홈 UI + +통합 대시보드는 프로젝트 선택 항목이 아니라 전역 홈 버튼으로 접근한다. + +- 홈 버튼: `/main/overview` +- 프로젝트 선택: 개별 프로젝트 화면 이동 전용 +- 프로젝트 대시보드·피드백·이슈·설정: `projectId` 필요 + +이렇게 분리하면 프로젝트 선택값을 바꾸지 않고도 여러 프로젝트의 Todo를 처리할 수 있고, 기존 프로젝트별 작업 흐름도 유지된다. + +## 9. 1차 구현 작업 순서 + +1. 통합 대시보드 API의 요청·응답 DTO 정의 +2. 프로젝트 접근 권한 범위 조회 로직 구현 +3. 여러 프로젝트 피드백·이슈 집계 서비스 구현 +4. `/main/overview` 페이지 추가 +5. 전체 요약 카드 구현 +6. 프로젝트별 요약 테이블 구현 +7. 기간·프로젝트·상태·내 담당 필터 구현 +8. 로그인 후 기본 라우팅 분기 구현 +9. 전역 홈 버튼 및 프로젝트 선택 UI 분리 +10. Todo 작업 목록과 행 단위 명령 처리 구현 +11. 이슈·Gitea 상태 동기화 API 연결 +12. 권한·라우팅·집계 테스트 작성 + +## 10. 향후 확장 + +부서별 또는 서비스군별로 프로젝트를 직접 묶고, 상위 관리자가 그룹을 관리해야 하는 요구가 생기면 별도 그룹 엔티티를 추가한다. + +```text +portfolio + └── projects +``` + +현재 단계에서는 DB에 상위 프로젝트 개념을 추가하지 않고, 사용자의 프로젝트 접근 권한을 기반으로 한 가상 통합 대시보드로 구현한다. + +## 11. 1차 구현 상태 + +전역 관리자 작업공간의 1차 기능을 적용했다. 프로젝트별 화면과 기존 권한 검증은 유지하고, `/main/overview`에서 접근 가능한 프로젝트의 Todo를 통합 조회·처리한다. + +- [x] `/api/admin/dashboard/overview` 집계 API에 `todos` 응답 추가 +- [x] 관리자별 프로젝트 접근 권한 검증 +- [x] 프로젝트별 피드백·이슈 집계 +- [x] `/main/overview` 전역 작업공간 화면 추가 +- [x] 홈 버튼으로 전역 화면 접근, 프로젝트 선택과 분리 +- [x] 전체 요약 카드 및 프로젝트별 요약 테이블 유지 +- [x] 기간·프로젝트·담당자 지정·피드백 상태·이슈 연결 필터 추가 +- [x] 피드백 담당자 지정·해제 +- [x] 피드백 상태 변경 +- [x] 관리자 댓글 등록 +- [x] 이슈 연결·해제 및 내부 이슈 6단계 상태 변경 +- [x] Gitea 이슈 검색·연결 및 외부 상태 새로고침 +- [x] 내부 이슈 상태 변경 시 연결된 Gitea 이슈 동기화 시도 +- [x] 피드백 상태와 이슈 상태를 독립적으로 처리하는 구조 유지 + +현재 Todo 목록은 프로젝트별 최근 피드백을 최대 500건까지 집계하며, 대량 데이터에 대한 서버 페이지네이션·가상 스크롤은 후속 작업이다. 내부 메모·일괄 처리와 이슈 처리 탭의 추가 일괄 작업은 기존 프로젝트별 API 권한을 재사용해 다음 단계로 확장한다. diff --git a/docs/관리페이지 md 파일/naver-works-notification-policy.md b/docs/관리페이지 md 파일/naver-works-notification-policy.md new file mode 100644 index 0000000..df68b1a --- /dev/null +++ b/docs/관리페이지 md 파일/naver-works-notification-policy.md @@ -0,0 +1,103 @@ +# 네이버웍스 피드백 알림 정책 + +- 작성일: 2026-08-28 +- 적용 범위: ABC UserFeedback 피드백 알림 +- 목적: 피드백 이벤트를 필요한 사용자에게만 전달하고, 중복·오발송·민감정보 노출을 방지한다. + +## 1. 확정된 이벤트별 수신자 + +| 이벤트 | 수신자 | 발송 조건 | +| --- | --- | --- | +| 신규 피드백 등록 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | 피드백이 실제로 새로 등록된 경우 | +| 피드백 상태 변경 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | 이전 상태와 현재 상태가 실제로 다른 경우 | +| 관리자 공개 댓글 등록 | 해당 피드백 작성자 | 공개 댓글이 실제로 등록된 경우 | + +### 수신자 해석 기준 + +- 피드백 담당자는 해당 피드백에 지정된 담당자다. +- 프로젝트 관리자는 해당 프로젝트·채널에 `PROJECT_MANAGER`로 지정된 활성 사용자다. +- 피드백 작성자는 SSO의 작성자 계정과 네이버웍스 사용자 매핑이 확인된 경우에만 수신 대상이 된다. +- 수신자 정보는 이벤트 payload의 임의 이메일을 그대로 사용하지 않고, 서버가 프로젝트·채널·피드백 데이터로 확정한다. +- 동일 사용자가 여러 역할에 해당하면 네이버웍스 알림은 한 번만 발송한다. +- 담당자, 프로젝트 관리자, 작성자 중 지정되지 않았거나 비활성인 사용자는 수신 대상에서 제외한다. +- 시스템 관리자라는 이유만으로 모든 프로젝트 알림을 받지는 않는다. 해당 프로젝트의 프로젝트 관리자 또는 피드백 담당자로 지정된 경우에만 받는다. + +## 2. 알림을 보내지 않는 이벤트 + +- 내부 메모 등록·수정·삭제: 외부 사용자에게 발송하지 않는다. +- 관리자 공개 댓글 수정·삭제: 별도 정책 확정 전까지 발송하지 않는다. +- 이슈 상태 변경 및 Gitea 상태 변경: 피드백 알림과 독립적인 이벤트로 취급하며, 별도 수신자·메시지 정책 확정 전까지 이 정책으로 발송하지 않는다. +- 피드백과 이슈의 상태는 서로 독립적이다. 이슈 상태가 바뀌었다고 피드백 상태 변경 알림을 보내지 않는다. +- 같은 상태로 다시 저장한 경우 상태 변경 알림을 보내지 않는다. +- 알림 연동 Bot 또는 시스템 계정이 생성한 이벤트는 재귀 알림 방지를 위해 관리자 댓글 알림 대상에서 제외한다. + +## 3. 이벤트별 발송 규칙 + +### 3.1 신규 피드백 + +- `feedback.created` 이벤트만 처리한다. +- 담당자·프로젝트 관리자·작성자를 합친 뒤 이메일 또는 사용자 ID 기준으로 중복 제거한다. +- 담당자와 프로젝트 관리자는 Secretary DB의 프로젝트·채널 권한 원본을 기준으로 조회한다. +- 작성자에게 보내는 메시지는 비밀글 여부를 확인하고, 비밀글의 제목·본문·댓글 원문을 포함하지 않는다. + +### 3.2 피드백 상태 변경 + +- `feedback.status_changed` 이벤트만 처리한다. +- `previousStatus`와 `currentStatus`가 같으면 발송하지 않는다. +- `완료`, `진행하지 않음`을 포함한 모든 유효한 피드백 상태 변경을 동일한 정책으로 처리한다. +- 메시지에는 피드백 ID, 변경 전 상태, 변경 후 상태, 상세 페이지 링크를 포함할 수 있다. +- 이슈 상태 변경이나 Gitea 동기화 결과만으로는 이 이벤트를 생성하지 않는다. + +### 3.3 관리자 공개 댓글 + +- `feedback.admin_comment_created` 이벤트만 처리한다. +- 수신자는 해당 피드백 작성자 한 명으로 제한한다. +- 담당자, 프로젝트 관리자, 시스템 관리자, 기본 네이버웍스 방에는 보내지 않는다. +- 댓글 작성자가 내부 메모를 등록한 경우 이 이벤트를 생성하지 않는다. +- 댓글 본문은 길이 제한과 평문화 후 전송하고, 민감정보가 포함되지 않도록 최소 정보 원칙을 적용한다. + +## 4. 중복·재전송 방지 + +- ABC가 제공한 `eventId`를 우선 사용한다. +- `eventId + 수신자 + 알림 템플릿 버전`을 발송 중복 판단 키로 사용한다. +- 동일 이벤트를 다시 수신해도 같은 수신자에게 한 번만 발송한다. +- 이벤트 처리 성공과 네이버웍스 발송 성공은 별도로 기록한다. +- 네트워크 오류, timeout, `408`, `429`, `5xx`만 제한적으로 재시도한다. +- `400`, `403`, 잘못된 사용자 매핑 등 영구 오류는 재시도하지 않는다. +- 오래된 이벤트나 현재 ABC 데이터와 다른 이벤트는 현재 데이터를 재조회한 뒤 무시하거나 격리한다. + +## 5. 오발송 차단 및 개인정보 보호 + +- 사용자별 Bot 메시지 발송을 기본으로 하며, 기본 방 전체 발송으로 대체하지 않는다. +- 프로젝트·채널 범위를 벗어난 관리자나 수신자에게 발송하지 않는다. +- 수신자 매핑에 실패하면 임의 사용자나 전체 방으로 보내지 않고 실패 이력만 남긴다. +- 메시지에는 업무에 필요한 최소 정보만 포함한다. +- SSO 토큰, Webhook 인증값, 네이버웍스 access token·client secret·private key는 메시지와 로그에 남기지 않는다. +- 전화번호, IP 주소, MAC 주소는 기본 알림 메시지에 포함하지 않는다. +- 비밀글은 권한이 확인된 상세 링크와 최소 식별 정보만 전송한다. +- Webhook 인증 실패 요청의 원문 payload와 개인정보는 로그에 기록하지 않는다. + +## 6. 메시지 기본 구성 + +알림 메시지는 다음 항목을 기본으로 사용한다. + +- 이벤트 종류 +- 프로젝트명·채널명 +- 피드백 ID +- 피드백 제목 또는 상태 변경 정보 +- 변경 전·후 상태(상태 변경 이벤트인 경우) +- 댓글 내용 일부(관리자 공개 댓글인 경우) +- 피드백 상세 페이지 링크 + +비밀글 또는 개인정보가 포함될 수 있는 경우 제목·본문·댓글 원문을 생략한다. + +## 7. 운영 확인 항목 + +- [ ] 프로젝트 관리자가 Secretary DB에서 해당 프로젝트·채널의 `PROJECT_MANAGER`로 활성 지정되어 있다. +- [ ] 피드백 담당자와 작성자의 네이버웍스 계정 매핑이 확인되어 있다. +- [ ] 신규 피드백 등록 시 담당자·프로젝트 관리자·작성자에게 각각 한 번만 도착한다. +- [ ] 피드백 상태를 `완료` 또는 `진행하지 않음`으로 변경했을 때도 한 번만 도착한다. +- [ ] 관리자 공개 댓글은 작성자에게만 도착한다. +- [ ] 내부 메모와 이슈 상태 변경은 이 정책에 따라 발송되지 않는다. +- [ ] 같은 상태 재저장, Webhook 재전송, 동일 수신자 중복 등록 시 중복 메시지가 발생하지 않는다. +- [ ] 수신자 매핑 실패 시 전체 방으로 잘못 발송되지 않는다. diff --git a/docs/관리페이지 md 파일/naver-works-webhook-notification-tasks.md b/docs/관리페이지 md 파일/naver-works-webhook-notification-tasks.md new file mode 100644 index 0000000..ae540f2 --- /dev/null +++ b/docs/관리페이지 md 파일/naver-works-webhook-notification-tasks.md @@ -0,0 +1,429 @@ +# ABC Webhook–네이버웍스 알림 연동 작업 계획 + +- 작성일: 2026-08-26 +- 목적: ABC UserFeedback에서 발생한 피드백 이벤트를 웹훅으로 수신하고, 네이버웍스 Bot API를 통해 관리자에게 알림을 보낸다. +- 1차 범위: 신규 피드백 등록, 피드백 상태 변경, 관리자 공개 댓글 등록 +- 현재 상태: 핵심 구현 완료, 로컬 검증 완료. 네이버웍스 실발송과 ABC 외부 Webhook 실연동은 자격증명·운영 설정 후 검증 필요 + +## 1. 목표 시나리오 + +```text +ABC UserFeedback + -> ABC Webhook 이벤트 발송 + -> 우리 백엔드 웹훅 수신 API + -> 서명·토큰 검증 및 중복 요청 검사 + -> 이벤트 표준화 + -> 알림 대상자·메시지 결정 + -> 네이버웍스 Bot API 호출 + -> 발송 결과와 실패 이력 저장 +``` + +### 1.1 1차 이벤트 + +| 이벤트 | 알림 제목 | 기본 수신 대상 | +| --------------------- | ---------------- | --------------------------------------------- | +| 신규 피드백 등록 | 신규 피드백 등록 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | +| 피드백 상태 변경 | 피드백 상태 변경 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | +| 관리자 공개 댓글 등록 | 관리자 댓글 등록 | 해당 피드백 작성자만 | + +내부 메모는 일반 댓글과 공개 범위가 다르므로 1차 범위에서는 알림 대상에서 제외한다. 내부 메모를 네이버웍스로 보내려면 관리자 전용 수신 정책을 별도로 확정해야 한다. + +## 2. 현재 구조 확인 사항 + +- [x] NestJS API에 프로젝트별 Webhook 등록·조회·수정·삭제 API가 존재함 +- [x] Webhook에 이벤트 종류와 프로젝트·채널 범위를 설정하는 구조가 존재함 +- [x] 실제 피드백 등록·상태 변경·관리자 공개 댓글 등록 이벤트를 내부 알림 흐름에 연결 +- [x] 외부 Webhook 수신 전용 라우터 구현 +- [ ] 현재 Webhook payload 형식과 이벤트별 예시 확보 +- [x] Webhook 인증 방식(서명 또는 토큰) 구현 +- [x] 네이버웍스 Bot API 인증·발송 어댑터 구현 + +현재 Webhook 관리 API가 있다는 것만으로는 이벤트 발송과 네이버웍스 전송이 완성된 상태가 아니다. 발송 지점과 payload 계약을 먼저 확인한 뒤 수신 API를 연결한다. + +## 3. 권장 구현 구조 + +### 3.1 웹훅 수신 계층 + +추후 이벤트가 늘어나도 라우터를 변경하지 않도록 단일 수신 엔드포인트에서 처리한다. + +권장 경로 예시: + + POST /api/integrations/abc/webhooks + +수신 처리 순서: + +1. 원문 body와 인증 헤더를 확보한다. +2. Webhook 토큰 또는 서명을 검증한다. +3. `eventId` 또는 payload hash로 중복 요청을 검사한다. +4. 이벤트 타입을 허용 목록과 비교한다. +5. 이벤트별 payload를 내부 표준 이벤트로 변환한다. +6. 빠르게 `2xx` 응답을 반환하고 알림 발송을 처리한다. +7. 발송 결과를 로그에 기록하고 실패 시 재시도 대상으로 남긴다. + +인증에 실패한 요청은 상세 payload를 로그에 남기지 않고 `401` 또는 `403`으로 응답한다. 이벤트 처리 실패와 네이버웍스 일시 장애는 Webhook 자체 인증 실패와 구분한다. + +### 3.2 표준 이벤트 모델 + +외부 ABC payload에 직접 의존하지 않고 다음과 같은 내부 모델로 변환한다. + +```ts +type NotificationEventType = + | 'feedback.created' + | 'feedback.status_changed' + | 'feedback.admin_comment_created'; + +interface NotificationEvent { + eventId: string; + type: NotificationEventType; + occurredAt: string; + projectId: number; + channelId: number; + feedbackId: number; + title?: string; + feedbackUrl?: string; + actor?: { + id?: string; + name?: string; + email?: string; + }; + status?: { + previous?: string; + current: string; + }; + comment?: { + id?: number; + content: string; + isInternal: boolean; + }; +} +``` + +이벤트 변환기와 메시지 템플릿을 분리해 ABC payload가 변경되거나 이벤트가 추가되어도 네이버웍스 클라이언트는 수정하지 않도록 한다. + +### 3.3 네이버웍스 연동 계층 + +다음 인터페이스를 기준으로 네이버웍스 구현체를 분리한다. + +```ts +interface NotificationSender { + send(message: NotificationMessage): Promise; +} +``` + +권장 구성: + +- `WebhookReceiver`: ABC Webhook 검증·수신 +- `NotificationEventMapper`: 외부 payload를 표준 이벤트로 변환 +- `NotificationRouter`: 이벤트별 수신자 결정 +- `NaverWorksClient`: 인증 토큰 발급·갱신 및 메시지 API 호출 +- `NaverWorksMessageBuilder`: 이벤트별 메시지 조합 +- `NotificationLogService`: 요청·성공·실패·재시도 이력 기록 + +현재 라우팅 구현은 SSO/ABC 피드백 데이터의 이메일을 NAVER WORKS 로그인 ID로 사용한다. 신규 피드백과 상태 변경은 `assignee_email`, Secretary DB에서 프로젝트·채널 매핑으로 조회한 `PROJECT_MANAGER`, ABC 프로젝트 멤버의 관리자 이메일, `requester_email`을 합친 뒤 대소문자 기준으로 중복 제거한다. 관리자 공개 댓글은 `requester_email`만 대상으로 한다. 사용자별 발송은 `/bots/{botId}/users/{userId}/messages`를 사용하고 기본 방 발송으로 대체하지 않는다. 프로젝트 관리자 지정의 원본은 Secretary DB이며, ABC API는 `x-api-key`로 보호된 내부 수신자 조회 API를 통해서만 이를 읽는다. + +## 4. 필요한 환경변수 + +실제 변수명은 기존 환경변수 규칙에 맞춰 확정한다. 비밀값은 코드, `.env.example`, README, 채팅에 입력하지 않는다. + +### 4.1 ABC Webhook 수신 + +```dotenv +ABC_WEBHOOK_ENABLED=false +ABC_WEBHOOK_PATH=/api/integrations/abc/webhooks +ABC_WEBHOOK_TOKEN= +ABC_WEBHOOK_SIGNING_SECRET= +ABC_WEBHOOK_ALLOWED_PROJECT_IDS= +SUPPORT_API_BASE_URL=http://127.0.0.1:8010 +MASTER_API_KEY= +``` + +`TOKEN` 방식과 서명 방식 중 ABC가 실제로 제공하는 인증 방법만 활성화한다. 둘 다 지원할 경우 운영에서는 서명 검증을 우선한다. + +### 4.2 네이버웍스 Bot API + +```dotenv +NAVER_WORKS_ENABLED=false +NAVER_WORKS_API_BASE_URL= +NAVER_WORKS_AUTH_URL= +NAVER_WORKS_BOT_ID= +NAVER_WORKS_DOMAIN_ID= +NAVER_WORKS_CLIENT_ID= +NAVER_WORKS_CLIENT_SECRET= +NAVER_WORKS_SERVICE_ACCOUNT= +NAVER_WORKS_PRIVATE_KEY= +NAVER_WORKS_DEFAULT_ROOM_ID= +NAVER_WORKS_DEFAULT_USER_ID= +``` + +필요한 값은 네이버웍스 인증 방식에 따라 달라질 수 있다. 특히 `client secret`, 서비스 계정, 개인키는 Gitea Secrets 또는 스테이징 서버의 비밀 환경변수로만 등록한다. + +### 4.3 알림 처리 정책 + +```dotenv +NOTIFICATION_DELIVERY_MODE=async +NOTIFICATION_MAX_RETRIES=3 +NOTIFICATION_RETRY_BASE_DELAY_MS=1000 +NOTIFICATION_LOG_ENABLED=true +NOTIFICATION_INCLUDE_FEEDBACK_URL=true +NOTIFICATION_MAX_BODY_LENGTH=10000 +NOTIFICATION_RATE_LIMIT_PER_MINUTE=60 +NOTIFICATION_CIRCUIT_BREAKER_FAILURES=5 +NOTIFICATION_KILL_SWITCH=false +``` + +## 5. 알림 정책 + +### 5.1 수신 대상 + +확정된 1차 수신 정책은 다음과 같다. + +| 이벤트 | 수신 대상 | 발송 기준 | +| --------------------- | --------------------------------------------- | ------------------------------------------------ | +| 신규 피드백 등록 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | 동일 사용자가 여러 역할에 해당하면 한 번만 발송 | +| 피드백 상태 변경 | 피드백 담당자, 프로젝트 관리자, 피드백 작성자 | 실제 상태가 변경된 경우에만 발송 | +| 관리자 공개 댓글 등록 | 해당 피드백 작성자 | 담당자·프로젝트 관리자·기본 방에는 발송하지 않음 | + +- 피드백 담당자와 프로젝트 관리자는 해당 프로젝트·채널 범위에 한정한다. +- 피드백 작성자는 SSO 사용자와 네이버웍스 계정 매핑이 확인된 경우에만 발송한다. +- 동일 사용자가 담당자·프로젝트 관리자·작성자 역할을 여러 개 가지고 있어도 중복 발송하지 않는다. +- 담당자 또는 프로젝트 관리자가 지정되지 않은 경우 해당 역할로는 발송하지 않는다. +- 작성자 계정 매핑에 실패한 경우 임의의 사용자나 전체 방으로 대체 발송하지 않고 실패 이력으로 남긴다. +- 비밀글의 사용자 알림에는 제목·본문·댓글 내용을 포함하지 않고, 권한 검증이 가능한 상세 링크와 최소 정보만 사용한다. + +### 5.2 이벤트 조건 + +- 실제 상태가 변경된 경우에만 발송할지 +- 상태를 같은 값으로 다시 저장할 때는 발송하지 않을지 +- 관리자 공개 댓글만 발송하고 내부 메모는 제외할지 +- 댓글 수정·삭제도 알림을 보낼지 +- 이슈 상태 변경과 Gitea 이벤트를 1차 범위에 포함할지 + +### 5.3 메시지 정책 + +기본 메시지에는 다음 정보를 포함하는 것을 권장한다. + +- 프로젝트명·채널명 +- 피드백 ID +- 피드백 제목 +- 변경 전·후 상태 +- 댓글 작성자와 댓글 내용 일부 +- 피드백 상세 페이지 링크 + +비밀글은 네이버웍스 메시지에 본문을 포함하지 않고, 권한이 있는 사용자가 상세 페이지에서 확인하도록 한다. 댓글은 개인정보와 긴 본문 노출을 막기 위해 길이 제한과 마스킹 정책을 둔다. + +### 5.4 비정상 Webhook 차단 정책 + +Webhook은 인터넷에 노출될 수 있는 입력 경계로 취급한다. 아래 조건을 만족하지 않는 요청은 이벤트 처리와 네이버웍스 발송을 진행하지 않는다. + +| 상황 | 처리 방법 | 목적 | +| --------------------------------------- | ------------------------------------------------ | ------------------------------ | +| 인증 토큰·서명 없음 | `401/403` 응답, payload 미기록 | 위조 요청 차단 | +| 서명 불일치 | `401/403` 응답, 보안 이벤트만 기록 | payload 변조 차단 | +| 서명 timestamp가 허용 시간 초과 | 거부 | 캡처 payload 재전송 차단 | +| 지원하지 않는 `eventType` | 인증 후 `202`로 무시하고 이벤트 타입만 기록 | 불필요한 재시도·오류 폭주 방지 | +| 필수 식별자 누락 | `400`, 알림 미발송 | 잘못된 대상 발송 방지 | +| 존재하지 않는 프로젝트·채널·피드백 | 검증 후 무시 또는 격리 | 삭제·오래된 데이터 오발송 방지 | +| 허용 목록 밖의 프로젝트 | 무시, 보안 로그 기록 | 다른 프로젝트 데이터 유출 방지 | +| 허용 목록 밖의 수신자 | 발송하지 않고 격리 | 관리자 외 대상 오발송 방지 | +| 너무 큰 body 또는 잘못된 Content-Type | `413/415` | 리소스 고갈·파싱 공격 방지 | +| 같은 이벤트 재수신 | idempotency key로 한 번만 발송 | 중복 메시지 방지 | +| 오래된 이벤트 또는 순서가 뒤바뀐 이벤트 | event version 검증 후 무시 또는 최신 상태 재조회 | 과거 상태로 되돌아간 알림 방지 | + +인증 실패 요청에는 원문 body, 댓글 내용, 이메일, IP/MAC 등의 개인정보를 로그로 남기지 않는다. 반복적인 인증 실패는 IP·경로 단위 rate limit 또는 차단 대상으로 분류한다. + +### 5.5 이벤트 유효성 및 중복 처리 + +- 이벤트 식별자는 ABC가 제공하는 `eventId`를 우선 사용하고, 없으면 `payload hash + occurredAt` 조합으로 생성한다. +- 중복 기준은 `eventId + notification target + template version`으로 한다. 수신자별 발송이 필요한 경우 한 이벤트를 대상자마다 한 번만 처리한다. +- 상태 변경은 `previousStatus`와 `currentStatus`가 같으면 알림을 보내지 않는다. +- 이벤트가 현재 ABC 데이터와 일치하지 않으면 payload만 믿지 않고 피드백 상세를 재조회한다. +- 이미 더 최신 상태가 처리된 경우 오래된 이벤트는 폐기한다. +- 댓글 이벤트는 댓글 ID를 기준으로 중복 처리하며, 수정·삭제 이벤트는 별도 허용 목록에 추가하기 전까지 무시한다. +- Webhook 수신 성공과 네이버웍스 발송 성공은 별도 상태로 기록한다. 수신 성공을 발송 성공으로 간주하지 않는다. + +### 5.6 수신자 및 권한 안전장치 + +- 수신자는 이벤트 payload가 지정한 임의의 주소를 그대로 사용하지 않고, 프로젝트·채널·담당자 정보를 기준으로 서버에서 결정한다. +- 네이버웍스 방 ID와 사용자 ID는 프로젝트별 allowlist에 등록된 값만 사용한다. +- 담당자 정보가 없거나 비활성 사용자인 경우 전체 관리자 방으로 자동 전송하지 않는다. 별도 격리 대상 또는 명시된 기본 운영자에게만 보낸다. +- 작성자에게 알림을 보낼 때는 SSO 계정과 네이버웍스 계정의 매핑이 확인된 경우에만 발송한다. +- 비밀글의 제목·본문·댓글 내용은 사용자용 방에 포함하지 않는다. 필요한 경우 관리자 전용 방에 최소 정보만 보낸다. +- 내부 메모는 공개 댓글과 다른 이벤트로 취급하고, 관리자 전용 정책이 확정되기 전까지 외부 알림을 금지한다. +- 프로젝트별 환경과 수신자 설정을 분리해 다른 프로젝트의 알림 방으로 섞이지 않도록 한다. + +### 5.7 재시도·장애·폭주 제어 + +- 재시도 대상은 네트워크 오류, timeout, `408`, `429`, `5xx`로 제한한다. +- `400`, `403`, 잘못된 수신자 등 영구 오류는 재시도하지 않고 실패 이력과 원인을 저장한다. +- `401`은 토큰을 한 번 갱신한 뒤 한 차례만 재요청한다. 반복 인증 실패는 즉시 중단한다. +- 지수 backoff와 jitter를 사용하고, 최대 재시도 횟수를 넘으면 격리 큐 또는 재처리 목록에 저장한다. +- 네이버웍스 장애가 지속되면 circuit breaker를 열어 즉시 실패시키고, ABC Webhook 수신 자체는 정상적으로 종료한다. +- 짧은 시간에 이벤트가 급증하면 사용자별·프로젝트별·전체 발송량 제한을 적용한다. +- 동일 피드백의 연속 상태 변경은 짧은 시간 동안 묶음 알림 또는 마지막 상태 알림으로 축약할 수 있다. 단, 이 정책은 업무상 누락이 허용되는지 확인한 뒤 적용한다. +- 실패한 발송을 무한 재시도하지 않으며, 관리자에게 재처리 필요 건수만 별도로 알린다. + +### 5.8 무한 루프 및 자기 자신이 만든 이벤트 차단 + +- 네이버웍스 발송 결과나 Bot이 남긴 메시지가 ABC 피드백 이벤트로 되돌아오는 구조인지 확인한다. +- 이벤트 actor가 연동용 Bot 또는 시스템 계정이면 관리자 댓글 알림 대상에서 제외한다. +- 발신 Webhook과 수신 Webhook URL이 동일하거나 서로 재호출하는 구성이 되지 않도록 배포 시 검사한다. +- 알림 메시지에 포함된 링크 조회는 이벤트를 다시 생성하지 않는 읽기 전용 경로를 사용한다. +- 연동별 `source`와 `correlationId`를 기록해 동일 연동에서 발생한 재귀 호출을 탐지한다. + +### 5.9 개인정보·로그·운영 안전 + +- 네이버웍스 메시지에는 업무 처리에 필요한 최소 정보만 포함한다. +- 이메일, 전화번호, IP, MAC 주소, SSO 토큰, Webhook 원문 인증값은 메시지와 일반 로그에서 제외한다. +- 댓글 본문은 최대 길이를 제한하고 HTML·스크립트·제어문자를 제거한 평문으로 변환한다. +- 로그에는 `eventId`, 프로젝트 ID, 피드백 ID, 처리 상태, 실패 코드, correlation ID만 기본 저장한다. +- access token, client secret, private key, Webhook secret은 로그·에러 응답·Swagger 예시에 절대 포함하지 않는다. +- 스테이징 Bot·방과 운영 Bot·방의 자격증명 및 수신자 allowlist를 분리한다. +- 긴급 상황에서 신규 발송만 중지할 수 있는 `NOTIFICATION_KILL_SWITCH`를 제공하고, 수신 이벤트와 실패 이력은 보존한다. +- 설정 변경과 kill switch 활성화·해제 이력은 관리자 감사 로그에 남긴다. + +### 5.10 ACK 및 오류 응답 정책 + +Webhook 제공자가 재전송하는 조건을 고려해 응답을 다음과 같이 고정한다. + +| 조건 | 응답 | 후속 처리 | +| ----------------------- | --------: | ---------------------------------- | +| 인증 실패 | `401/403` | 처리하지 않음 | +| body 형식 오류 | `400` | 처리하지 않음 | +| 허용되지 않은 이벤트 | `202` | 무시 이력만 저장 | +| 인증된 신규 이벤트 접수 | `202` | 비동기 발송 | +| 중복 이벤트 | `200/202` | 기존 결과 재사용, 재발송하지 않음 | +| 내부 큐 저장 실패 | `503` | 제공자 재전송 유도, 장애 로그 저장 | + +네이버웍스 발송이 실패했다는 이유로 이미 수신한 Webhook을 무조건 `5xx`로 응답하지 않는다. 그렇지 않으면 ABC가 같은 이벤트를 반복 전송하여 중복 알림이 발생할 수 있다. + +## 6. 구현 전 확정 항목 + +다음 항목은 구현 전에 업무 담당자와 확정한다. + +- [x] 신규 피드백·상태 변경·댓글별 최종 수신 대상 +- 담당자 미지정·계정 매핑 실패·프로젝트 설정 누락 시의 격리 대상 +- 피드백 상태 변경과 이슈 상태 변경을 같은 알림으로 묶을지 여부 +- 비밀글·내부 메모의 네이버웍스 전송 허용 범위 +- 상태가 짧은 시간에 여러 번 바뀔 때 즉시 발송할지 묶음 발송할지 +- 댓글 본문 최대 길이와 개인정보 마스킹 규칙 +- 실패 알림을 네이버웍스 외 별도 채널로 보낼지 여부 +- 발송 로그 보관 기간과 재처리 권한 +- 스테이징과 운영의 Bot, 방, 수신자, Secret 분리 방식 + +## 7. 작업 단계 + +### Phase 1. 연동 계약 확인 + +- [x] ABC Webhook 이벤트 타입과 내부 이벤트 이름 추가 +- [ ] 신규 등록·상태 변경·댓글 등록 payload 샘플 확보 +- [x] Webhook 수신 인증 방식 구현: HMAC 서명 우선, 토큰 방식 지원 +- [x] 네이버웍스 Bot API 인증 방식과 메시지 API 확인 +- [ ] 관리자 방 또는 사용자별 수신자 식별자 확보 +- [ ] 스테이징용 네이버웍스 Bot과 테스트 방 지정 + +### Phase 2. 웹훅 수신 API + +- [x] Webhook 수신 DTO와 이벤트 허용 목록 작성 +- [x] 토큰·서명 검증 구현 +- [x] timestamp·body size·Content-Type 기본 검증 구현 +- [x] 프로젝트 allowlist 검증 구현 +- [x] 요청 ID와 중복 이벤트 방지 구현 +- [ ] 오래된 이벤트·순서 역전·동일 상태 변경 차단 구현 +- [x] 이벤트 표준화 및 메시지 변환 구현 +- [x] 인증 실패·잘못된 payload·처리 실패 오류 응답 정의 +- [x] 수신 API를 Swagger에서 제외해 인증정보·운영 입력값 노출 방지 + +### Phase 3. 네이버웍스 클라이언트 + +- [x] 액세스 토큰 발급·캐시·만료 갱신 구현 +- [x] Bot 메시지 발송 함수 구현 +- [x] API rate limit·일시 오류 기본 재시도 처리 +- [ ] 4xx·5xx별 재시도 정책과 circuit breaker 구현 +- [ ] 테스트용 발송 기능 구현 +- [x] 비밀값이 로그에 노출되지 않도록 구현 + +### Phase 4. 이벤트별 알림 + +- [x] 신규 피드백 메시지 템플릿 구현 +- [x] 상태 변경 메시지 템플릿 구현 +- [x] 피드백 상태 변경 저장 경로에서 실제 변경 시 상태 이벤트 발생 구현 +- [x] 관리자 공개 댓글 메시지 템플릿 구현 +- [x] 담당자·프로젝트 관리자·피드백 작성자 라우팅 구현 +- [x] 이벤트별 중복 수신자 제거 및 계정 매핑 실패 격리 처리 구현 +- [x] 비밀글·내부 메모 공개 범위 처리 +- [x] 상세 페이지 링크 생성 + +### Phase 5. 발송 이력과 장애 대응 + +- [x] 알림 발송 로그 모델 추가 +- [x] 성공·실패·재시도 상태 저장 +- [x] 중복 발송 방지용 idempotency key 저장 +- [x] 네이버웍스 장애 시 원본 이벤트 유실 방지 +- [ ] 발송량 제한·폭주 제어·kill switch 구현 +- [x] 내부 메모리·Bot 자기 이벤트의 외부 알림 차단 +- [ ] 관리자 화면 또는 운영 로그에서 실패 건 확인 가능하게 구성 + +### Phase 6. 검증 및 배포 + +- [ ] 단위 테스트: 서명 검증·이벤트 변환·템플릿·라우팅 +- [ ] 통합 테스트: Webhook 수신부터 네이버웍스 client mock 발송까지 +- [ ] 스테이징 실제 Bot 방으로 신규 피드백 테스트 +- [ ] 상태 변경 중복 발송 테스트 +- [ ] 공개 댓글과 내부 메모의 수신 범위 테스트 +- [ ] 네이버웍스 인증 실패·timeout·재시도 테스트 +- [ ] 위조 서명·오래된 timestamp·중복·순서 역전 이벤트 테스트 +- [ ] 허용되지 않은 프로젝트·수신자·비밀글·내부 메모 차단 테스트 +- [ ] 폭주·circuit breaker·kill switch·재처리 테스트 +- [x] Gitea Actions의 Variables/Secrets를 스테이징 API 컨테이너까지 전달하도록 배포 설정 연결 +- [ ] 스테이징 환경변수와 HTTPS 외부 Webhook URL 확인 +- [ ] 운영 전 비밀값 회전 및 테스트 로그 정리 + +### 6.1 로컬 적용 및 검증 결과 (2026-08-26) + +- [x] `notification_deliveries` 마이그레이션 로컬 적용 확인 +- [x] `GET http://127.0.0.1:4000/api/health` 응답 `200`, database `up` 확인 +- [x] `POST http://127.0.0.1:4000/api/integrations/abc/webhooks` 응답 `202`, 로컬 발송 비활성 응답 확인 +- [x] API·웹 타입체크 및 린트 통과 +- [x] 기존 Webhook listener 단위 테스트 22건 통과 +- [x] 완료·진행하지 않음을 포함한 상태 변경 이벤트 중복 발송 방지 로직 보완 및 API 정적 검사 통과 +- [ ] 네이버웍스 실제 Bot 메시지 발송 확인 — 로컬 자격증명 미설정 +- [ ] ABC에서 외부 Webhook을 실제로 보내는 end-to-end 확인 — ABC 관리자 설정 필요 + +## 8. 완료 기준 + +- [ ] 허용된 ABC Webhook만 인증 후 처리된다. +- [ ] 신규 피드백 등록 시 지정된 네이버웍스 수신자에게 한 번만 알림이 도착한다. +- [ ] 피드백 상태가 실제로 변경될 때 이전 상태와 현재 상태가 포함된 알림이 도착한다. +- [ ] 관리자 공개 댓글 등록 시 정책에 맞는 수신자에게 알림이 도착한다. +- [ ] 내부 메모가 작성자에게 노출되지 않는다. +- [ ] 네이버웍스 장애가 발생해도 Webhook 요청과 발송 실패 이력이 유실되지 않는다. +- [x] 토큰·시크릿·개인키가 소스, API 응답, 로그에 노출되지 않는다. +- [ ] 새로운 이벤트를 추가할 때 수신 라우터의 핵심 흐름을 변경하지 않고 이벤트 매퍼·라우터·템플릿만 추가할 수 있다. + +## 9. 사용자가 별도로 해야 하는 작업 + +1. 네이버웍스 Bot과 테스트용 방을 만들고 Bot을 방에 초대한다. +2. 네이버웍스 개발자 콘솔에서 Bot 메시지 발송 권한과 인증정보를 발급한다. +3. 로컬 `.env`에 네이버웍스 인증정보를 직접 입력한다. 값은 저장소·README·채팅에 올리지 않는다. +4. 로컬 검증 시 다음 플래그를 켠 뒤 API를 재시작한다. + + ABC_WEBHOOK_ENABLED=true + ABC_WEBHOOK_TOKEN= + NAVER_WORKS_ENABLED=true + NAVER_WORKS_BOT_ID= + NAVER_WORKS_DEFAULT_ROOM_ID=<테스트 방 ID> + + HMAC 서명을 사용할 경우 `ABC_WEBHOOK_TOKEN` 대신 `ABC_WEBHOOK_SIGNING_SECRET`을 설정한다. + +5. ABC 관리자 화면에서 Webhook URL을 `https://<외부주소>/api/integrations/abc/webhooks`로 등록하고, 신규 피드백·상태 변경·관리자 공개 댓글 이벤트를 선택한다. +6. 신규 피드백 등록, 피드백 상태 변경, 관리자 공개 댓글 등록을 각각 1회씩 실행해 네이버웍스 수신 여부와 `notification_deliveries` 기록을 확인한다. +7. 내부 메모, 비밀글, 중복 Webhook, 잘못된 토큰 요청이 알림으로 전송되지 않는지 확인한다. + +## 10. 우선 결정할 입력값 + +구현 시작 전에 다음 네 가지만 먼저 확정한다. + +1. 네이버웍스 수신 방식: 특정 방 1개인지, 사용자별 Bot 메시지인지 +2. 신규 피드백·상태 변경·댓글별 수신 대상 +3. ABC Webhook의 실제 payload와 인증 방식 +4. 스테이징에서 사용할 외부 Webhook URL과 네이버웍스 테스트 방 diff --git a/docs/관리페이지 md 파일/post-id-strategy-and-performance.md b/docs/관리페이지 md 파일/post-id-strategy-and-performance.md new file mode 100644 index 0000000..bd0398f --- /dev/null +++ b/docs/관리페이지 md 파일/post-id-strategy-and-performance.md @@ -0,0 +1,383 @@ +# 다중 프로젝트 피드백 플랫폼의 ID 및 성능 설계 + +## 핵심 결론 + +현재 시스템은 프로젝트마다 작성 서버가 따로 있고, 각 서버가 중앙 관리 API를 통해 하나의 DB에 글을 저장하는 구조임. + +이 구조에서는 중앙 DB가 번호를 발급하기를 기다리는 방식보다, **각 작성 서버가 글 ID를 먼저 만들고 중앙 API로 보내는 방식**이 적합함. + +### 최종 권장안 + +1. **1순위: ULID 또는 UUID v7 선발급** + - 프로젝트 작성 서버에서 ID를 먼저 생성함 + - 화면 ID, API ID, DB ID를 동일하게 사용할 수 있음 + - 네트워크 재시도에도 같은 글로 판단해 중복 등록을 막을 수 있음 + - 여러 서버에 별도 Worker 번호를 배정하지 않아도 됨 + +2. **숫자 ID가 반드시 필요할 때: Snowflake** + - 여러 서버에서 숫자 ID를 선발급할 수 있음 + - ULID보다 저장 공간이 작음 + - 대신 Worker ID와 서버 시간 동기화 관리가 필요함 + +3. **기존 구조를 가장 적게 바꿀 때: BIGINT Auto Increment** + - 구현과 인덱스 성능은 가장 단순함 + - 중앙 DB 의존성과 재시도 중복을 별도로 해결해야 함 + - 화면 ID와 DB ID가 달라질 수 있음 + +### 선택 기준 한눈에 보기 + +| 우선순위 | 선택 | +|---|---| +| 여러 작성 서버의 독립성, 재시도 중복 방지, ID 일치 | ULID / UUID v7 | +| 숫자 ID, 분산 선발급, 높은 쓰기량 | Snowflake | +| 변경 최소화, 단일 DB, 중앙 저장 완료 후 ID 발급 | BIGINT Auto Increment | + +--- + +## 1. 현재 시스템 구조 + +```text +[프로젝트 A 작성 서버] ─┐ +[프로젝트 B 작성 서버] ─┼─ API ─▶ [중앙 관리 서버] ─▶ [중앙 MySQL] +[프로젝트 C 작성 서버] ─┘ +``` + +- 피드백 작성 페이지 서버는 프로젝트마다 별도로 운영함 +- 각 작성 서버가 중앙 관리 API를 호출함 +- 중앙 관리 서버가 피드백을 한곳에 저장하고 관리함 +- 향후 외부 Q&A 서버도 같은 API로 연결할 수 있음 + +여러 서버가 데이터를 만들고 하나의 서버가 모아 관리하는 **다중 발행자·단일 집계 구조**임. + +## 2. 현재 구조에서 발생하는 문제 + +### 2.1 네트워크 재시도에 따른 중복 등록 + +```text +1. 프로젝트 A가 중앙 API로 글을 전송함 +2. 중앙 서버는 저장했지만 응답이 네트워크 지연으로 늦어짐 +3. 프로젝트 A는 실패로 판단하고 같은 글을 다시 전송함 +4. 중앙 DB가 매번 새 번호를 발급하면 같은 글이 2건 저장됨 +``` + +작성 서버가 ID를 먼저 만들고 재시도할 때 같은 ID를 사용하면 중복을 막을 수 있음. + +```text +같은 글 + 같은 ID = 같은 요청 +``` + +중앙 API와 DB에는 해당 ID를 PK 또는 유일 키로 설정해야 함. 이를 멱등성(Idempotency)이라고 함. + +### 2.2 중앙 DB 번호 발급 의존 + +Auto Increment 방식에서는 중앙 DB가 저장하면서 번호를 발급함. + +- 작성 서버는 저장이 끝나야 글 번호를 알 수 있음 +- 여러 프로젝트의 등록 요청이 중앙 DB에 집중됨 +- 중앙 DB 또는 네트워크 장애가 번호 발급과 저장에 함께 영향을 줌 +- 비동기 전송과 재시도 처리가 복잡해짐 + +### 2.3 프로젝트 구분 + +중앙 목록에서 숫자만 표시하면 출처 프로젝트를 바로 알기 어려움. + +```text +PRJA_01K8A9V4N5J9F28D5G3H1A2B3C +PRJB_01K8A9V4N5J9F28D5G3H1A2B3D +``` + +화면 ID와 DB ID를 일치시키려면 위의 전체 식별자를 DB에 그대로 저장해야 함. 프로젝트 코드와 ULID를 분리 저장한 뒤 화면에서 조합하면 화면 ID와 DB ID가 달라질 수 있음. + +## 3. 식별자 선택 기준 + +| 기준 | 확인할 내용 | +|---|---| +| 중복 방지 | 네트워크 재시도에도 같은 글로 인식되는지 | +| 선발급 | 중앙 DB 저장 전에 작성 서버에서 ID를 만들 수 있는지 | +| 분산 생성 | 여러 서버가 동시에 만들어도 충돌하지 않는지 | +| 정렬 성능 | 시간 순 생성으로 B-Tree 인덱스에 유리한지 | +| 저장 크기 | PK와 외래키 인덱스가 과도하게 커지지 않는지 | +| 화면 가독성 | 사람이 읽고 프로젝트를 구분하기 쉬운지 | +| 일관성 | DB, API, URL, 화면 ID가 같은지 | +| 운영 난이도 | 서버별 추가 설정과 관리가 필요한지 | + +## 4. 식별자 체계 7가지 비교 + +| 번호 | 방식 | 저장 스펙 | 인덱스 특성 | 장점 | 단점 | 적합한 환경 | +|---:|---|---|---|---|---|---| +| 1 | `INT AUTO_INCREMENT` | `INT` 4바이트 | 작고 빠름 | 구현이 가장 쉬움, 메모리 효율이 좋음 | 약 21억 개 한계, 중앙 DB 의존, 프로젝트 구분 어려움 | 소규모 단일 서버 | +| 2 | `BIGINT AUTO_INCREMENT` | `BIGINT` 8바이트 | 순차 입력에 유리 | 번호 범위가 매우 큼, 구현이 쉬움 | 중앙 DB에서 번호 발급, 프로젝트 구분 어려움 | 단일 DB 기반 대용량 시스템 | +| 3 | `BIGINT + 프로젝트 코드 조합` | DB는 `BIGINT` | 정수 인덱스 유지 | 저장 성능과 화면 식별성을 함께 확보 | 실제 ID와 화면 조합 ID가 달라질 수 있음 | 중앙 DB 구조와 운영 편의성을 함께 중시하는 경우 | +| 4 | 비즈니스 문자열 직접 저장 | `VARCHAR` | 정수보다 큼 | DB만 봐도 프로젝트와 글을 구분 가능 | 문자열 인덱스 증가, 동시 채번 문제 | 트래픽이 적고 가독성이 중요한 경우 | +| 5 | `ULID / UUID v7` | `BINARY(16)` 또는 문자열 | 시간 순 입력에 유리 | 서버별 선발급, 분산 생성, 재시도 멱등성에 적합 | 정수보다 크고 사람이 읽기 어려움 | 여러 프로젝트 서버가 API로 전송하는 구조 | +| 6 | Snowflake | `BIGINT` 8바이트 | 시간 순 입력에 유리 | 분산 서버에서 숫자 ID를 선발급할 수 있음 | Worker ID와 시계 동기화 관리 필요 | 매우 높은 쓰기량의 분산 시스템 | +| 7 | UUID v4 | UUID 또는 `VARCHAR(36)` | 랜덤 입력으로 페이지 분할 가능 | 생성이 쉽고 추측이 어려움 | 인덱스가 크고 랜덤 삽입으로 쓰기 효율이 낮아질 수 있음 | 보안상 추측 방지가 최우선인 경우 | + +### 대략적인 PK 인덱스 크기 + +1,000만 건 기준의 대략적인 비교임. 실제 크기는 외래키, 보조 인덱스, 페이지 여유 공간에 따라 달라짐. + +| 방식 | 대략적인 크기 | +|---|---:| +| `INT` | 약 200MB | +| `BIGINT` | 약 400MB | +| `BINARY(16)` | 약 800MB 이상 | +| `VARCHAR(36)` UUID | 약 1~2GB 이상 | + +## 5. 현재 시스템에 맞는 3가지 케이스 + +### Case 1. ULID 또는 UUID v7을 실제 글 ID로 사용 + +#### 구성 + +```text +DB ID / API ID / 화면 ID: +PRJA_01K8A9V4N5J9F28D5G3H1A2B3C +``` + +- 각 프로젝트 작성 서버가 ID를 먼저 생성함 +- 글 데이터와 ID를 중앙 API로 전송함 +- 중앙 DB는 ID를 PK 또는 UNIQUE KEY로 확인함 +- 동일 ID의 재요청은 중복 저장하지 않음 +- 관리페이지는 전달받은 ID를 그대로 표시함 + +#### 장점 + +- 프로젝트 서버가 중앙 DB에 의존하지 않고 ID를 선발급할 수 있음 +- 네트워크 타임아웃 후 재시도해도 중복 등록을 막기 쉬움 +- 시간 순 ID라 UUID v4보다 B-Tree 인덱스에 유리함 +- 서버별 Worker ID를 배정할 필요가 없음 +- 화면 ID와 DB ID를 동일하게 유지할 수 있음 + +#### 단점 + +- `BIGINT`보다 PK와 외래키 인덱스가 큼 +- 사람이 숫자 하나로 기억하거나 구두 전달하기 어려움 +- 프로젝트 접두사를 포함하면 ID가 더 길어짐 + +#### 권장 저장 방식 + +화면 ID와 DB ID를 완전히 일치시켜야 하면 접두사와 ULID를 합친 전체 문자열을 `VARCHAR`에 저장함. + +```text +PRJA_01K8A9V4N5J9F28D5G3H1A2B3C +``` + +DB 용량을 줄이기 위해 ULID를 `BINARY(16)`으로 분리 저장하면 화면에서 조합한 값과 DB의 실제 값이 달라질 수 있음. + +### Case 2. Snowflake 숫자 ID 사용 + +#### 구성 + +```text +DB ID / API ID / 화면 ID: +178293849182394880 +``` + +Snowflake는 다음 값을 조합해 64비트 숫자를 생성함. + +```text +시간 정보 + Worker ID + 같은 시간 안의 순번 +``` + +#### 장점 + +- 여러 서버에서 중앙 DB 의존 없이 숫자 ID를 선발급할 수 있음 +- ULID보다 PK와 외래키 인덱스가 작음 +- 시간 순 정렬이 가능함 +- 화면 ID와 DB ID를 동일하게 유지할 수 있음 + +#### 단점 + +- 프로젝트와 서버마다 고유 Worker ID를 배정해야 함 +- 서버가 늘어나거나 컨테이너가 추가될 때 번호 관리가 필요함 +- Worker ID가 중복되면 ID 충돌이 발생함 +- 서버 시간 오차를 관리해야 함 +- 현재 등록량에서는 운영 복잡도가 성능 이점보다 클 수 있음 + +### Case 3. BIGINT Auto Increment + 프로젝트 코드 별도 표시 + +#### 구성 + +```text +DB ID: 10492 +프로젝트: ABC +채널: WEB +화면 표시: ABC-WEB-10492 +``` + +- DB: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY` +- 프로젝트와 채널은 별도 컬럼으로 저장함 +- 화면과 알림에서 프로젝트 코드와 숫자를 조합함 + +#### 장점 + +- 정수 PK와 외래키로 인덱스가 작음 +- 기존 구조를 가장 적게 변경함 +- 중앙 DB에서 구현하기 쉬움 +- 순차 입력으로 B-Tree 페이지 분할이 적음 + +#### 단점 + +- 중앙 DB에 저장되어야 ID를 알 수 있음 +- 재시도 중복을 막으려면 별도의 Idempotency Key가 필요함 +- 화면의 `ABC-WEB-10492`와 DB의 `10492`가 다름 +- 프로젝트별 번호가 1번부터 시작하지 않음 + +## 6. ULID와 Snowflake를 쉽게 비교 + +### Snowflake + +각 지점에 번호표 기계를 설치하고 지점 번호를 미리 배정하는 방식임. + +- 프로젝트 A 서버는 Worker ID 1 +- 프로젝트 B 서버는 Worker ID 2 +- 프로젝트 C 서버는 Worker ID 3 + +번호표가 작고 빠르지만 서버가 늘어날 때마다 번호를 관리해야 함. 서버 시계가 맞지 않으면 번호 발급에 문제가 생길 수 있음. + +### ULID + +각 지점이 별도 등록 없이 앱을 실행해 고유 번호표를 만드는 방식임. + +- Worker ID를 따로 배정하지 않아도 됨 +- 여러 서버에서 동시에 생성해도 충돌 가능성이 매우 낮음 +- 네트워크가 끊겨 같은 글을 다시 보내도 같은 ID로 중복을 확인할 수 있음 +- 시간 순으로 생성되어 DB에 정리하기 쉬움 + +현재처럼 프로젝트별 작성 서버가 여러 곳에 나뉘어 있으면 관리할 설정이 적은 ULID가 운영 측면에서 유리함. + +## 7. 피드백 테이블 인덱스 설계 + +### 주요 조회 조건 + +```sql +WHERE channel_id = ? + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20 +``` + +### 기본 후보 + +```sql +CREATE INDEX idx_feedbacks_channel_active_list +ON feedbacks (channel_id, deleted_at, id DESC); +``` + +`deleted_at`을 포함한 이유는 소프트 삭제된 글을 목록에서 제외하는 조건까지 인덱스 탐색 범위에 포함하기 위해서임. + +다만 삭제된 글이 거의 없으면 다음 인덱스가 더 작고 효율적일 수도 있음. + +```sql +CREATE INDEX idx_feedbacks_channel_list +ON feedbacks (channel_id, id DESC); +``` + +두 방식 중 어느 쪽이 좋은지는 삭제 데이터 비율과 실제 실행 계획으로 확인해야 함. + +### 미확인 피드백 조회 + +```sql +CREATE INDEX idx_feedbacks_unread +ON feedbacks (channel_id, admin_first_read_at, deleted_at); +``` + +### JSON 내부 값 조회 + +`data` 내부의 `category`를 자주 검색한다면 생성 컬럼과 인덱스를 구성할 수 있음. + +```sql +ALTER TABLE feedbacks +ADD COLUMN category VARCHAR(50) +GENERATED ALWAYS AS (data->>'$.category') VIRTUAL; +``` + +```sql +CREATE INDEX idx_feedbacks_channel_category +ON feedbacks (channel_id, category, id DESC); +``` + +## 8. 5만 건 기준 메모리와 캐시 + +현재 523행의 데이터 용량이 약 `528.0 KiB`라면 다음과 같이 추정할 수 있음. + +| 항목 | 예상 크기 | +|---|---:| +| 5만 건 데이터 | 약 50~70MB | +| 복합 인덱스 2~3개 | 약 4~6MB | +| 데이터와 인덱스 합계 | 약 80MB 미만 | + +실제 크기는 JSON 또는 TEXT 데이터 길이, 인덱스 자료형, 보조 인덱스 수에 따라 달라짐. + +### InnoDB Buffer Pool + +```text +[MySQL 데이터와 인덱스] + │ + ▼ +[InnoDB Buffer Pool 메모리 상주] + │ + ▼ +[반복 조회 시 디스크 접근 감소] +``` + +Buffer Pool이 데이터와 인덱스보다 충분히 크면 자주 조회되는 페이지가 메모리에 상주할 수 있음. 다만 실제 상주율과 응답 시간은 서버 메모리, 설정, 실행 계획, 동시 접속자 수에 따라 달라짐. + +### Redis 애플리케이션 캐시 + +```text +[클라이언트] + │ + ▼ +[Redis 캐시] ── HIT ──▶ 캐시 결과 반환 + │ + MISS + ▼ +[중앙 API] ──▶ [MySQL] ──▶ Redis 갱신 +``` + +우선 캐싱할 대상: + +- 프로젝트·채널별 최신 피드백 목록 +- 각 목록의 첫 페이지 +- 전체 피드백 수와 상태별 개수 +- 답변 대기·미확인 요약 정보 + +캐시 키 예시: + +```text +feedback:project:{project_id}:channel:{channel_id}:page:1 +``` + +새 글 등록, 수정, 삭제가 발생하면 관련 캐시를 삭제하거나 갱신해야 함. + +| 방식 | 설명 | +|---|---| +| 즉시 삭제 | 변경 시 관련 캐시를 바로 삭제함 | +| 짧은 TTL | 30초~1분 후 자동 만료함 | +| 조합 | 첫 페이지는 즉시 삭제하고 나머지는 TTL을 사용함 | + +## 9. 성능 점검 방법 + +인덱스를 적용하기 전에 실제 실행 계획을 확인해야 함. + +```sql +EXPLAIN ANALYZE +SELECT * +FROM feedbacks +WHERE channel_id = 1 + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20; +``` + +다음 항목을 확인함. + +- 예상한 인덱스가 선택되는지 +- `key`에 복합 인덱스가 표시되는지 +- `rows`가 과도하게 많지 않은지 +- 전체 테이블 스캔이 발생하지 않는지 +- `Using filesort` 또는 `Using temporary`가 발생하는지 + +인덱스 적용 전후의 실행 시간, 읽은 행 수, DB CPU와 디스크 I/O를 함께 비교해야 함. diff --git a/docs/관리페이지 md 파일/post-id-strategy-and-performance.previous.md b/docs/관리페이지 md 파일/post-id-strategy-and-performance.previous.md new file mode 100644 index 0000000..82af06e --- /dev/null +++ b/docs/관리페이지 md 파일/post-id-strategy-and-performance.previous.md @@ -0,0 +1,418 @@ +# 게시판 글 ID 및 피드백 성능 설계 + +게시판 글 ID(Primary Key 및 식별자)는 조회 성능, 보안, 분산 환경 지원 여부, URL 가독성 등에 따라 여러 방식으로 설계할 수 있음. + +## 1. 자동 증가 정수형 + +### Auto Increment / Sequence + +MySQL의 `AUTO_INCREMENT`, PostgreSQL의 `BIGSERIAL` 또는 `IDENTITY`를 사용하는 전통적인 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `1`, `2`, `3`, `4`, ... | +| 저장 방식 | `INT` 또는 `BIGINT` | +| 구현 난이도 | 가장 낮음 | +| 인덱스 성능 | 매우 좋음 | + +### 장점 + +- 구현이 가장 쉽고 직관적임 +- 저장 용량이 적음 +- B-Tree 인덱스 정렬 성능이 좋음 +- 최신 글 순서대로 자동 정렬하기 쉬움 + +### 단점 + +- 다음 글 번호를 쉽게 추측할 수 있음 +- 크롤링이나 글 수량 파악에 취약할 수 있음 +- 분산 DB 또는 멀티 마스터 환경에서 충돌 없이 발급하기 어려움 + +### 적합한 사용처 + +- 사내 인트라넷 +- 관리자 전용 게시판 +- 단일 DB 인스턴스 환경 + +## 2. UUID v4 + +### 완전 랜덤 128비트 식별자 + +전 세계에서 고유한 값을 무작위로 생성하는 표준 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `c9a646d3-9c61-4cc9-bc19-482386a37365` | +| 저장 방식 | UUID 또는 `CHAR(36)` 등 | +| 생성 방식 | 완전 랜덤 | +| 분산 환경 | 지원 | + +### 장점 + +- 중복 가능성이 매우 낮음 +- 중앙 조율 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 +- 분산 서버 환경에 적합함 + +### 단점 + +- 36자 문자열이라 URL이 길고 복잡함 +- 완전 랜덤 값이라 B-Tree 인덱스 페이지 분할이 자주 발생할 수 있음 +- 대량 입력 환경에서 정수형보다 쓰기 성능이 낮을 수 있음 + +### 적합한 사용처 + +- 외부 API 통신 식별자 +- 추측 방지가 중요한 비즈니스 데이터 +- 분산 시스템 + +## 3. 시간 순 정렬 가능 고유 식별자 + +### UUID v7 / ULID + +UUID의 고유성과 Auto Increment의 시간 정렬 장점을 결합한 방식. + +| 방식 | 예시 | +|---|---| +| UUID v7 | `018d3b2f-7a54-7c3a-8b1e-9d2a4e5b6c7d` | +| ULID | `01ARZ3NDEKTSV4RRFFQ69G5FAV` | + +### 장점 + +- 생성된 시간 순으로 정렬 가능함 +- B-Tree 인덱스에 순차적으로 적재되어 쓰기 성능이 좋음 +- 분산 환경에서 충돌 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 + +### 단점 + +- 정수형보다 저장 공간을 많이 사용함 +- UUID v4보다 생성 규칙이 복잡함 +- ID만 보고 생성 시점을 일부 추정할 수 있음 + +### 적합한 사용처 + +- 대용량 트래픽 게시판 +- 분산 서비스 +- 최신 웹 애플리케이션 + +## 4. 트위터 스노우플레이크 + +### Snowflake + +대량의 데이터를 분산 환경에서 생성하기 위해 고안된 64비트 정수 기반 방식. + +### 기본 구조 + +```text +타임스탬프 41비트 + 데이터센터/머신 ID 10비트 + 시퀀스 번호 12비트 +``` + +### 예시 + +```text +1542893478912349184 +``` + +### 장점 + +- 64비트 정수라 인덱싱 성능이 좋음 +- 시간 순 정렬이 가능함 +- 분산 서버 간 ID 충돌을 방지할 수 있음 +- 대규모 시스템에서 높은 처리량을 지원함 + +### 단점 + +- 별도의 ID 생성기 또는 Worker 관리가 필요함 +- 데이터센터 및 머신 ID 관리가 필요함 +- JavaScript에서 일반 숫자로 처리하면 정밀도 손실이 발생할 수 있음 +- Auto Increment보다 운영 복잡도가 높음 + +### 적합한 사용처 + +- 대규모 분산 SNS +- 초대형 커뮤니티 +- 초당 수천 건 이상의 글이 등록되는 서비스 + +## 5. 해시 기반 난독화 + +### Sqids / Hashids + +DB에는 정수형 ID를 저장하고, URL이나 외부 화면에서는 별도의 문자열로 변환하여 표시하는 방식. + +| 구분 | 예시 | +|---|---| +| 내부 DB ID | `1204` | +| 외부 URL ID | `b9Xq` | + +### 장점 + +- DB에서는 정수형 인덱스 성능을 유지할 수 있음 +- 짧고 깔끔한 URL을 만들 수 있음 +- 외부에서 원래 순번을 바로 유추하기 어려움 +- 기존 정수형 DB 구조를 유지하면서 외부 노출만 변경할 수 있음 + +### 단점 + +- 내부 ID와 외부 표시 ID가 달라짐 +- 암호화가 아니라 인코딩에 가까움 +- 시크릿 솔트가 노출되면 원래 정수값을 역산할 수 있음 +- 운영자가 같은 글을 서로 다른 ID로 인식할 수 있음 + +### 적합한 사용처 + +- 외부 URL을 짧게 만들고 싶은 서비스 +- 기존 정수형 DB를 유지하면서 외부 노출만 바꾸려는 서비스 +- 짧은 URL 식별자가 필요한 서비스 + +## 6. 비즈니스 접두사 결합형 + +### Stripe 스타일 ID + +글의 종류나 도메인을 식별할 수 있도록 ID 앞에 접두사를 붙이는 방식. + +### 예시 + +```text +post_01ARZ3NDEKTSV4RRFFQ69G5FAV +qna_01ARZ3NDEKTSV4RRFFQ69G5FAV +fb_202609_a8f3d +``` + +### 장점 + +- ID만 보고도 데이터 종류를 구분할 수 있음 +- 로그나 알림에서 식별이 쉬움 +- API 중심 서비스에 적합함 +- 디버깅과 모니터링이 편리함 + +### 단점 + +- 숫자만 사용하는 방식보다 ID가 길어짐 +- 글 종류가 변경되면 접두사 정책도 고려해야 함 +- 단순한 게시판에서는 다소 복잡할 수 있음 + +### 적합한 사용처 + +- API 중심 B2B SaaS +- 여러 유형의 게시물이 함께 존재하는 시스템 +- 로그와 웹훅에서 데이터 유형을 즉시 구분해야 하는 서비스 + +## 방식별 비교 + +| 방식 | 저장 타입 | URL 길이 | 분산 환경 | 인덱스 성능 | 추측 방지 | 적합한 상황 | +|---|---|---:|:---:|:---:|:---:|---| +| Auto Increment | `INT` / `BIGINT` | 매우 짧음 | 낮음 | 매우 좋음 | 낮음 | 내부 관리용, 소규모 서비스 | +| UUID v4 | UUID / `CHAR` | 김 | 높음 | 보통 | 높음 | 보안이 중요한 데이터 | +| UUID v7 / ULID | UUID / 16바이트 등 | 중간 | 높음 | 좋음 | 높음 | 대용량·분산 웹 서비스 | +| Snowflake | `BIGINT` | 짧음 | 높음 | 매우 좋음 | 보통 | 대규모 분산 시스템 | +| Sqids / Hashids | DB는 정수형 | 외부는 짧음 | 낮음 | 매우 좋음 | 보통 | URL 난독화가 필요한 경우 | +| 접두사 결합형 | 문자열 또는 조합형 | 중간~김 | 방식에 따라 다름 | 방식에 따라 다름 | 방식에 따라 다름 | API·로그 식별이 중요한 경우 | + +## 검토할 때 확인할 기준 + +ID 방식을 선택할 때는 다음 항목을 함께 확인해야 함. + +1. 화면에 표시되는 ID와 DB의 실제 ID를 같게 유지할 것인지 +2. 프로젝트마다 번호를 새로 시작할 것인지 +3. 프로젝트를 넘어선 전체 글 번호가 필요한지 +4. 피드백과 이슈가 같은 번호 체계를 사용할 것인지 +5. 외부 Q&A 원본 ID와 관리페이지 ID를 어떻게 연결할 것인지 +6. URL에서 ID를 직접 노출할 것인지 +7. 단일 서버인지 분산 서버인지 +8. 향후 글 수가 얼마나 증가할지 + +특히 내부 DB ID와 화면 표시 ID를 별도로 운영하면, 같은 글을 서로 다른 번호로 인식할 수 있으므로 표시 정책을 먼저 확정해야 함. + +--- + +# 피드백 테이블 성능 및 캐시 설계 + +## 1. 현재 테이블 구조 + +### 기본 구성 + +- 데이터베이스 엔진: MySQL 8.0.46 +- 스토리지 엔진: InnoDB +- 기본 컬럼 + - `id`: 기본 키 + - `created_at`: 등록 일시 + - `updated_at`: 수정 일시 + - `deleted_at`: 소프트 삭제 일시 + - `channel_id`: 채널 ID + - `data`: 제목, 내용, 카테고리 등을 저장하는 JSON 또는 TEXT 컬럼 + - `admin_first_read_at`: 관리자가 처음 확인한 일시 + - `admin_first_read_by`: 처음 확인한 관리자 + +### 주요 조회 패턴 + +1. 특정 채널의 삭제되지 않은 피드백을 최신순으로 조회 +2. 특정 채널에서 아직 확인하지 않은 피드백만 조회 +3. `data` 내부의 `category` 값으로 피드백 필터링 +4. 목록을 페이지 단위로 조회 + +## 2. 인덱스 설계 + +### 2.1 채널별 피드백 목록 조회 + +목록 조회는 다음과 같은 조건과 정렬을 사용함. + +```sql +WHERE channel_id = ? + AND deleted_at IS NULL +ORDER BY id DESC +``` + +`channel_id`, `deleted_at`, `id`를 하나의 복합 인덱스로 구성하면 채널과 활성 상태로 범위를 좁힌 뒤 최신순으로 바로 조회할 수 있음. + +```sql +CREATE INDEX idx_feedbacks_channel_active_list +ON feedbacks (channel_id, deleted_at, id DESC); +``` + +`id`가 `AUTO_INCREMENT`라면 `created_at` 대신 `id`를 정렬 기준으로 사용할 수 있음. 일반적으로 ID 증가 순서와 등록 순서가 일치하므로 정렬 비용을 줄이기 쉬움. + +### 2.2 미확인 피드백 조회 + +관리자가 아직 확인하지 않은 피드백을 자주 조회한다면 다음 인덱스를 사용할 수 있음. + +```sql +CREATE INDEX idx_feedbacks_unread +ON feedbacks (channel_id, admin_first_read_at, deleted_at); +``` + +이 인덱스는 특정 채널에서 `admin_first_read_at IS NULL` 조건을 사용하는 조회의 전체 테이블 스캔을 줄이는 데 도움을 줌. + +### 2.3 JSON 내부 값 조회 + +`category`처럼 `data` 내부의 값을 조건으로 자주 조회한다면 생성 컬럼을 추가해 인덱스를 구성할 수 있음. + +```sql +ALTER TABLE feedbacks +ADD COLUMN category VARCHAR(50) +GENERATED ALWAYS AS (data->>'$.category') VIRTUAL; +``` + +```sql +CREATE INDEX idx_feedbacks_channel_category +ON feedbacks (channel_id, category, id DESC); +``` + +생성 컬럼을 `VIRTUAL`로 만들면 별도의 값을 테이블에 저장하지 않고 조회 시 계산할 수 있음. 해당 값의 검색 빈도와 데이터 분포를 확인한 뒤 적용해야 함. + +## 3. 5만 건 기준 데이터 및 메모리 규모 + +현재 523행의 데이터 용량이 약 `528.0 KiB`라면 행 하나의 평균 크기는 약 `1 KiB` 수준임. + +| 항목 | 예상 크기 | +|---|---:| +| 5만 건 데이터 | 약 50~70MB | +| 복합 인덱스 2~3개 | 약 4~6MB | +| 데이터와 인덱스 합계 | 약 80MB 미만 | + +실제 크기는 JSON 또는 TEXT 데이터의 길이, 인덱스 컬럼의 자료형, 인덱스 페이지 여유 공간에 따라 달라질 수 있음. + +InnoDB Buffer Pool이 데이터와 인덱스보다 충분히 크다면 자주 조회되는 데이터가 메모리에 상주할 수 있음. 이 경우 반복 조회에서 디스크 I/O가 줄어들고 인덱스 기반 조회의 응답 시간이 짧아짐. + +다만 실제 성능은 다음 조건에 따라 달라짐. + +- 서버의 가용 메모리 +- `innodb_buffer_pool_size` 설정 +- 동시 접속자 수 +- 쿼리의 실제 실행 계획 +- JSON 또는 TEXT 컬럼의 평균 크기 +- 페이지 번호가 뒤로 갈수록 커지는 OFFSET 사용 여부 + +## 4. 캐시 구성 + +캐시는 MySQL 내부 캐시와 애플리케이션 외부 캐시로 나눌 수 있음. + +```text +[클라이언트 요청] + │ + ▼ +┌────────────────────────┐ +│ Redis 애플리케이션 캐시 │ +└──────────┬─────────────┘ + │ + HIT ──┤ 캐시된 목록 즉시 반환 + │ + MISS ▼ +┌─────────────────────────┐ +│ MySQL InnoDB Buffer Pool │ +└──────────┬──────────────┘ + │ + ▼ + DB 조회 후 Redis 갱신 +``` + +### 4.1 InnoDB Buffer Pool + +InnoDB Buffer Pool은 테이블 데이터와 인덱스 페이지를 메모리에 보관함. + +- 반복 조회 시 디스크 접근을 줄일 수 있음 +- 복합 인덱스가 있으면 필요한 범위만 빠르게 탐색할 수 있음 +- 데이터와 인덱스 전체 크기보다 충분히 큰 Buffer Pool이 필요함 +- Buffer Pool 크기는 서버의 다른 프로세스가 사용할 메모리를 고려해 설정해야 함 + +### 4.2 Redis 애플리케이션 캐시 + +동시 접속자가 많거나 동일한 목록을 반복 조회하는 경우 Redis를 추가할 수 있음. + +#### 우선 캐싱할 대상 + +- 채널별 최신 피드백 목록 +- 채널별 첫 페이지 목록 +- 전체 피드백 수 또는 상태별 개수 +- 답변 대기, 미확인 등 자주 표시되는 요약 정보 + +#### 캐시 키 예시 + +```text +feedback:channel:{channel_id}:page:1 +``` + +실제 키에는 프로젝트, 채널, 페이지 크기, 정렬 조건, 검색 조건 등 결과에 영향을 주는 값을 포함해야 함. + +#### 캐시 만료 및 무효화 + +새 피드백이 등록되거나 기존 피드백이 수정·삭제되면 해당 채널의 목록 캐시를 갱신해야 함. + +| 방식 | 설명 | +|---|---| +| 즉시 삭제 | 등록·수정·삭제 시 관련 캐시를 바로 삭제함 | +| 짧은 TTL | 캐시 만료 시간을 30초~1분 정도로 설정함 | +| 조합 | 중요 목록은 즉시 삭제하고 나머지는 짧은 TTL을 사용함 | + +첫 페이지는 새 글 등록의 영향을 가장 많이 받으므로 우선적으로 삭제하거나 갱신해야 함. + +## 5. 조회 성능 점검 방법 + +인덱스를 추가하기 전에 실제 쿼리의 실행 계획을 확인해야 함. + +```sql +EXPLAIN ANALYZE +SELECT * +FROM feedbacks +WHERE channel_id = 1 + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20; +``` + +다음 항목을 확인함. + +- 사용할 인덱스가 예상대로 선택되는지 +- `type`이 불필요한 전체 테이블 스캔인지 +- `key`에 복합 인덱스가 표시되는지 +- `rows`가 과도하게 많지 않은지 +- `Extra`에 `Using filesort` 또는 `Using temporary`가 발생하는지 + +## 6. 적용 시 주의사항 + +- 인덱스는 조회 속도를 높이지만 등록·수정·삭제 시 추가 비용이 발생함 +- 사용하지 않는 인덱스는 저장 공간과 쓰기 성능을 낭비함 +- `deleted_at`의 값 분포가 대부분 `NULL`이면 인덱스 효과가 제한될 수 있음 +- JSON 필드는 자주 검색하는 값만 생성 컬럼으로 분리하는 것이 좋음 +- `OFFSET`이 큰 페이지에서는 커서 기반 조회를 검토할 수 있음 +- 인덱스 적용 전후에 실제 운영 쿼리의 실행 계획과 응답 시간을 비교해야 함 diff --git a/docs/관리페이지 md 파일/post-id-strategy-and-performance.v2.previous.md b/docs/관리페이지 md 파일/post-id-strategy-and-performance.v2.previous.md new file mode 100644 index 0000000..b6cf008 --- /dev/null +++ b/docs/관리페이지 md 파일/post-id-strategy-and-performance.v2.previous.md @@ -0,0 +1,538 @@ +# 게시판 글 ID 및 피드백 성능 설계 + +게시판 글 ID(Primary Key 및 식별자)는 조회 성능, 보안, 분산 환경 지원 여부, URL 가독성 등에 따라 여러 방식으로 설계할 수 있음. + +## 1. 자동 증가 정수형 + +### Auto Increment / Sequence + +MySQL의 `AUTO_INCREMENT`, PostgreSQL의 `BIGSERIAL` 또는 `IDENTITY`를 사용하는 전통적인 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `1`, `2`, `3`, `4`, ... | +| 저장 방식 | `INT` 또는 `BIGINT` | +| 구현 난이도 | 가장 낮음 | +| 인덱스 성능 | 매우 좋음 | + +### 장점 + +- 구현이 가장 쉽고 직관적임 +- 저장 용량이 적음 +- B-Tree 인덱스 정렬 성능이 좋음 +- 최신 글 순서대로 자동 정렬하기 쉬움 + +### 단점 + +- 다음 글 번호를 쉽게 추측할 수 있음 +- 크롤링이나 글 수량 파악에 취약할 수 있음 +- 분산 DB 또는 멀티 마스터 환경에서 충돌 없이 발급하기 어려움 + +### 적합한 사용처 + +- 사내 인트라넷 +- 관리자 전용 게시판 +- 단일 DB 인스턴스 환경 + +## 2. UUID v4 + +### 완전 랜덤 128비트 식별자 + +전 세계에서 고유한 값을 무작위로 생성하는 표준 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `c9a646d3-9c61-4cc9-bc19-482386a37365` | +| 저장 방식 | UUID 또는 `CHAR(36)` 등 | +| 생성 방식 | 완전 랜덤 | +| 분산 환경 | 지원 | + +### 장점 + +- 중복 가능성이 매우 낮음 +- 중앙 조율 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 +- 분산 서버 환경에 적합함 + +### 단점 + +- 36자 문자열이라 URL이 길고 복잡함 +- 완전 랜덤 값이라 B-Tree 인덱스 페이지 분할이 자주 발생할 수 있음 +- 대량 입력 환경에서 정수형보다 쓰기 성능이 낮을 수 있음 + +### 적합한 사용처 + +- 외부 API 통신 식별자 +- 추측 방지가 중요한 비즈니스 데이터 +- 분산 시스템 + +## 3. 시간 순 정렬 가능 고유 식별자 + +### UUID v7 / ULID + +UUID의 고유성과 Auto Increment의 시간 정렬 장점을 결합한 방식. + +| 방식 | 예시 | +|---|---| +| UUID v7 | `018d3b2f-7a54-7c3a-8b1e-9d2a4e5b6c7d` | +| ULID | `01ARZ3NDEKTSV4RRFFQ69G5FAV` | + +### 장점 + +- 생성된 시간 순으로 정렬 가능함 +- B-Tree 인덱스에 순차적으로 적재되어 쓰기 성능이 좋음 +- 분산 환경에서 충돌 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 + +### 단점 + +- 정수형보다 저장 공간을 많이 사용함 +- UUID v4보다 생성 규칙이 복잡함 +- ID만 보고 생성 시점을 일부 추정할 수 있음 + +### 적합한 사용처 + +- 대용량 트래픽 게시판 +- 분산 서비스 +- 최신 웹 애플리케이션 + +## 4. 트위터 스노우플레이크 + +### Snowflake + +대량의 데이터를 분산 환경에서 생성하기 위해 고안된 64비트 정수 기반 방식. + +### 기본 구조 + +```text +타임스탬프 41비트 + 데이터센터/머신 ID 10비트 + 시퀀스 번호 12비트 +``` + +### 예시 + +```text +1542893478912349184 +``` + +### 장점 + +- 64비트 정수라 인덱싱 성능이 좋음 +- 시간 순 정렬이 가능함 +- 분산 서버 간 ID 충돌을 방지할 수 있음 +- 대규모 시스템에서 높은 처리량을 지원함 + +### 단점 + +- 별도의 ID 생성기 또는 Worker 관리가 필요함 +- 데이터센터 및 머신 ID 관리가 필요함 +- JavaScript에서 일반 숫자로 처리하면 정밀도 손실이 발생할 수 있음 +- Auto Increment보다 운영 복잡도가 높음 + +### 적합한 사용처 + +- 대규모 분산 SNS +- 초대형 커뮤니티 +- 초당 수천 건 이상의 글이 등록되는 서비스 + +## 5. 해시 기반 난독화 + +### Sqids / Hashids + +DB에는 정수형 ID를 저장하고, URL이나 외부 화면에서는 별도의 문자열로 변환하여 표시하는 방식. + +| 구분 | 예시 | +|---|---| +| 내부 DB ID | `1204` | +| 외부 URL ID | `b9Xq` | + +### 장점 + +- DB에서는 정수형 인덱스 성능을 유지할 수 있음 +- 짧고 깔끔한 URL을 만들 수 있음 +- 외부에서 원래 순번을 바로 유추하기 어려움 +- 기존 정수형 DB 구조를 유지하면서 외부 노출만 변경할 수 있음 + +### 단점 + +- 내부 ID와 외부 표시 ID가 달라짐 +- 암호화가 아니라 인코딩에 가까움 +- 시크릿 솔트가 노출되면 원래 정수값을 역산할 수 있음 +- 운영자가 같은 글을 서로 다른 ID로 인식할 수 있음 + +### 적합한 사용처 + +- 외부 URL을 짧게 만들고 싶은 서비스 +- 기존 정수형 DB를 유지하면서 외부 노출만 바꾸려는 서비스 +- 짧은 URL 식별자가 필요한 서비스 + +## 6. 비즈니스 접두사 결합형 + +### Stripe 스타일 ID + +글의 종류나 도메인을 식별할 수 있도록 ID 앞에 접두사를 붙이는 방식. + +### 예시 + +```text +post_01ARZ3NDEKTSV4RRFFQ69G5FAV +qna_01ARZ3NDEKTSV4RRFFQ69G5FAV +fb_202609_a8f3d +``` + +### 장점 + +- ID만 보고도 데이터 종류를 구분할 수 있음 +- 로그나 알림에서 식별이 쉬움 +- API 중심 서비스에 적합함 +- 디버깅과 모니터링이 편리함 + +### 단점 + +- 숫자만 사용하는 방식보다 ID가 길어짐 +- 글 종류가 변경되면 접두사 정책도 고려해야 함 +- 단순한 게시판에서는 다소 복잡할 수 있음 + +### 적합한 사용처 + +- API 중심 B2B SaaS +- 여러 유형의 게시물이 함께 존재하는 시스템 +- 로그와 웹훅에서 데이터 유형을 즉시 구분해야 하는 서비스 + +## 방식별 비교 + +| 방식 | 저장 타입 | URL 길이 | 분산 환경 | 인덱스 성능 | 추측 방지 | 적합한 상황 | +|---|---|---:|:---:|:---:|:---:|---| +| Auto Increment | `INT` / `BIGINT` | 매우 짧음 | 낮음 | 매우 좋음 | 낮음 | 내부 관리용, 소규모 서비스 | +| UUID v4 | UUID / `CHAR` | 김 | 높음 | 보통 | 높음 | 보안이 중요한 데이터 | +| UUID v7 / ULID | UUID / 16바이트 등 | 중간 | 높음 | 좋음 | 높음 | 대용량·분산 웹 서비스 | +| Snowflake | `BIGINT` | 짧음 | 높음 | 매우 좋음 | 보통 | 대규모 분산 시스템 | +| Sqids / Hashids | DB는 정수형 | 외부는 짧음 | 낮음 | 매우 좋음 | 보통 | URL 난독화가 필요한 경우 | +| 접두사 결합형 | 문자열 또는 조합형 | 중간~김 | 방식에 따라 다름 | 방식에 따라 다름 | 방식에 따라 다름 | API·로그 식별이 중요한 경우 | + +## 검토할 때 확인할 기준 + +ID 방식을 선택할 때는 다음 항목을 함께 확인해야 함. + +1. 화면에 표시되는 ID와 DB의 실제 ID를 같게 유지할 것인지 +2. 프로젝트마다 번호를 새로 시작할 것인지 +3. 프로젝트를 넘어선 전체 글 번호가 필요한지 +4. 피드백과 이슈가 같은 번호 체계를 사용할 것인지 +5. 외부 Q&A 원본 ID와 관리페이지 ID를 어떻게 연결할 것인지 +6. URL에서 ID를 직접 노출할 것인지 +7. 단일 서버인지 분산 서버인지 +8. 향후 글 수가 얼마나 증가할지 + +특히 내부 DB ID와 화면 표시 ID를 별도로 운영하면, 같은 글을 서로 다른 번호로 인식할 수 있으므로 표시 정책을 먼저 확정해야 함. + +--- + +# 피드백 테이블 성능 및 캐시 설계 + +## 1. 현재 테이블 구조 + +### 기본 구성 + +- 데이터베이스 엔진: MySQL 8.0.46 +- 스토리지 엔진: InnoDB +- 기본 컬럼 + - `id`: 기본 키 + - `created_at`: 등록 일시 + - `updated_at`: 수정 일시 + - `deleted_at`: 소프트 삭제 일시 + - `channel_id`: 채널 ID + - `data`: 제목, 내용, 카테고리 등을 저장하는 JSON 또는 TEXT 컬럼 + - `admin_first_read_at`: 관리자가 처음 확인한 일시 + - `admin_first_read_by`: 처음 확인한 관리자 + +### 주요 조회 패턴 + +1. 특정 채널의 삭제되지 않은 피드백을 최신순으로 조회 +2. 특정 채널에서 아직 확인하지 않은 피드백만 조회 +3. `data` 내부의 `category` 값으로 피드백 필터링 +4. 목록을 페이지 단위로 조회 + +## 2. 인덱스 설계 + +### 2.1 채널별 피드백 목록 조회 + +목록 조회는 다음과 같은 조건과 정렬을 사용함. + +```sql +WHERE channel_id = ? + AND deleted_at IS NULL +ORDER BY id DESC +``` + +`channel_id`, `deleted_at`, `id`를 하나의 복합 인덱스로 구성하면 채널과 활성 상태로 범위를 좁힌 뒤 최신순으로 바로 조회할 수 있음. + +```sql +CREATE INDEX idx_feedbacks_channel_active_list +ON feedbacks (channel_id, deleted_at, id DESC); +``` + +`id`가 `AUTO_INCREMENT`라면 `created_at` 대신 `id`를 정렬 기준으로 사용할 수 있음. 일반적으로 ID 증가 순서와 등록 순서가 일치하므로 정렬 비용을 줄이기 쉬움. + +### 2.2 미확인 피드백 조회 + +관리자가 아직 확인하지 않은 피드백을 자주 조회한다면 다음 인덱스를 사용할 수 있음. + +```sql +CREATE INDEX idx_feedbacks_unread +ON feedbacks (channel_id, admin_first_read_at, deleted_at); +``` + +이 인덱스는 특정 채널에서 `admin_first_read_at IS NULL` 조건을 사용하는 조회의 전체 테이블 스캔을 줄이는 데 도움을 줌. + +### 2.3 JSON 내부 값 조회 + +`category`처럼 `data` 내부의 값을 조건으로 자주 조회한다면 생성 컬럼을 추가해 인덱스를 구성할 수 있음. + +```sql +ALTER TABLE feedbacks +ADD COLUMN category VARCHAR(50) +GENERATED ALWAYS AS (data->>'$.category') VIRTUAL; +``` + +```sql +CREATE INDEX idx_feedbacks_channel_category +ON feedbacks (channel_id, category, id DESC); +``` + +생성 컬럼을 `VIRTUAL`로 만들면 별도의 값을 테이블에 저장하지 않고 조회 시 계산할 수 있음. 해당 값의 검색 빈도와 데이터 분포를 확인한 뒤 적용해야 함. + +## 3. 5만 건 기준 데이터 및 메모리 규모 + +현재 523행의 데이터 용량이 약 `528.0 KiB`라면 행 하나의 평균 크기는 약 `1 KiB` 수준임. + +| 항목 | 예상 크기 | +|---|---:| +| 5만 건 데이터 | 약 50~70MB | +| 복합 인덱스 2~3개 | 약 4~6MB | +| 데이터와 인덱스 합계 | 약 80MB 미만 | + +실제 크기는 JSON 또는 TEXT 데이터의 길이, 인덱스 컬럼의 자료형, 인덱스 페이지 여유 공간에 따라 달라질 수 있음. + +InnoDB Buffer Pool이 데이터와 인덱스보다 충분히 크다면 자주 조회되는 데이터가 메모리에 상주할 수 있음. 이 경우 반복 조회에서 디스크 I/O가 줄어들고 인덱스 기반 조회의 응답 시간이 짧아짐. + +다만 실제 성능은 다음 조건에 따라 달라짐. + +- 서버의 가용 메모리 +- `innodb_buffer_pool_size` 설정 +- 동시 접속자 수 +- 쿼리의 실제 실행 계획 +- JSON 또는 TEXT 컬럼의 평균 크기 +- 페이지 번호가 뒤로 갈수록 커지는 OFFSET 사용 여부 + +## 4. 캐시 구성 + +캐시는 MySQL 내부 캐시와 애플리케이션 외부 캐시로 나눌 수 있음. + +```text +[클라이언트 요청] + │ + ▼ +┌────────────────────────┐ +│ Redis 애플리케이션 캐시 │ +└──────────┬─────────────┘ + │ + HIT ──┤ 캐시된 목록 즉시 반환 + │ + MISS ▼ +┌─────────────────────────┐ +│ MySQL InnoDB Buffer Pool │ +└──────────┬──────────────┘ + │ + ▼ + DB 조회 후 Redis 갱신 +``` + +### 4.1 InnoDB Buffer Pool + +InnoDB Buffer Pool은 테이블 데이터와 인덱스 페이지를 메모리에 보관함. + +- 반복 조회 시 디스크 접근을 줄일 수 있음 +- 복합 인덱스가 있으면 필요한 범위만 빠르게 탐색할 수 있음 +- 데이터와 인덱스 전체 크기보다 충분히 큰 Buffer Pool이 필요함 +- Buffer Pool 크기는 서버의 다른 프로세스가 사용할 메모리를 고려해 설정해야 함 + +### 4.2 Redis 애플리케이션 캐시 + +동시 접속자가 많거나 동일한 목록을 반복 조회하는 경우 Redis를 추가할 수 있음. + +#### 우선 캐싱할 대상 + +- 채널별 최신 피드백 목록 +- 채널별 첫 페이지 목록 +- 전체 피드백 수 또는 상태별 개수 +- 답변 대기, 미확인 등 자주 표시되는 요약 정보 + +#### 캐시 키 예시 + +```text +feedback:channel:{channel_id}:page:1 +``` + +실제 키에는 프로젝트, 채널, 페이지 크기, 정렬 조건, 검색 조건 등 결과에 영향을 주는 값을 포함해야 함. + +#### 캐시 만료 및 무효화 + +새 피드백이 등록되거나 기존 피드백이 수정·삭제되면 해당 채널의 목록 캐시를 갱신해야 함. + +| 방식 | 설명 | +|---|---| +| 즉시 삭제 | 등록·수정·삭제 시 관련 캐시를 바로 삭제함 | +| 짧은 TTL | 캐시 만료 시간을 30초~1분 정도로 설정함 | +| 조합 | 중요 목록은 즉시 삭제하고 나머지는 짧은 TTL을 사용함 | + +첫 페이지는 새 글 등록의 영향을 가장 많이 받으므로 우선적으로 삭제하거나 갱신해야 함. + +## 5. 조회 성능 점검 방법 + +인덱스를 추가하기 전에 실제 쿼리의 실행 계획을 확인해야 함. + +```sql +EXPLAIN ANALYZE +SELECT * +FROM feedbacks +WHERE channel_id = 1 + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20; +``` + +다음 항목을 확인함. + +- 사용할 인덱스가 예상대로 선택되는지 +- `type`이 불필요한 전체 테이블 스캔인지 +- `key`에 복합 인덱스가 표시되는지 +- `rows`가 과도하게 많지 않은지 +- `Extra`에 `Using filesort` 또는 `Using temporary`가 발생하는지 + +## 6. 적용 시 주의사항 + +- 인덱스는 조회 속도를 높이지만 등록·수정·삭제 시 추가 비용이 발생함 +- 사용하지 않는 인덱스는 저장 공간과 쓰기 성능을 낭비함 +- `deleted_at`의 값 분포가 대부분 `NULL`이면 인덱스 효과가 제한될 수 있음 +- JSON 필드는 자주 검색하는 값만 생성 컬럼으로 분리하는 것이 좋음 +- `OFFSET`이 큰 페이지에서는 커서 기반 조회를 검토할 수 있음 +- 인덱스 적용 전후에 실제 운영 쿼리의 실행 계획과 응답 시간을 비교해야 함 + +--- + +# 식별자 체계 7대 조합의 트레이드오프 + +식별자 체계는 식별자 크기, B-Tree 인덱스와 페이지 분할, 메모리 버퍼 풀 상주율, 분산 환경에서의 생성 여부, 화면 가독성, 예상 병목 시점을 함께 고려해야 함. + +## 1. 7대 조합 비교 + +| 번호 | 조합 방식 | 물리 저장 스펙 | 1,000만 건 인덱스 크기 | 장점 | 단점 및 리스크 | 적합한 환경 | +|---:|---|---|---:|---|---|---| +| 1 | **INT Auto-Increment 단독** | `INT` (4B) | 약 200MB | 구현 난이도 최저, 인덱스 용량 최소, 메모리 캐시 효율이 좋음, JavaScript 숫자 변환 이슈가 없음 | 최대 약 21억 번호 한계, 화면에서 프로젝트 구분이 어려움, 번호 추측에 취약함 | 5만 건 이하의 소규모 내부 인트라넷 | +| 2 | **BIGINT Auto-Increment 단독** | `BIGINT` (8B) | 약 400MB | 매우 큰 번호 범위, 순차 적재로 페이지 분할이 적음, 구현 복잡도가 낮음 | 단순 숫자만 보여 프로젝트 식별성이 낮음, 멀티 마스터·샤딩 환경에서는 중앙 채번 병목 가능 | 단일 DB 기반 대용량 내부 시스템 | +| 3 | **BIGINT PK + 복합 코드 투트랙** | `BIGINT` (8B) + 화면 조합 | 약 400MB | DB 내부는 8바이트 정수로 유지, 화면과 알림에는 `ABC-WEB-104`처럼 식별성 있는 코드 사용, 별도 채번 락이 필요 없음 | 접두사 결합 로직 필요, ID 검색 시 코드 파싱 또는 검색 규칙 고려 필요 | 성능과 화면 식별성을 함께 요구하는 시스템 | +| 4 | **비즈니스 문자열 복합키 직접 저장** | `VARCHAR(30)` | 약 800MB~1.2GB | DB만 확인해도 `ABC-WEB-104`처럼 내용을 바로 파악 가능, 별도 변환 로직이 없음 | `MAX(번호)+1` 방식에서 동시성 락 병목 가능, 외래키 인덱스 용량 증가, 문자열 비교 비용 증가 | 트래픽이 적고 화면 가독성이 최우선인 관리자 도구 | +| 5 | **ULID / UUID v7 단독** | `BINARY(16)` 또는 `VARCHAR(26)` | 약 800MB~1GB | 시간순 정렬로 페이지 분할을 줄임, 분산 서버에서 별도 중앙 인프라 없이 생성 가능, 단일 컬럼으로 외부 노출 가능 | 8바이트 정수보다 인덱스가 큼, 사람이 구두로 식별하기 어려움, 문자열 저장 시 용량 증가 | 분산 MSA 환경, 오픈 API 중심 서비스 | +| 6 | **Snowflake** | `BIGINT` (8B) | 약 400MB | 8바이트 정수의 인덱스 효율 유지, 다중 서버에서 높은 동시 생성량 지원 | Worker ID 관리와 서버 시계 동기화 필요, 단일 DB 환경에서는 운영 복잡도가 높음 | 초당 수만 건의 쓰기가 발생하는 글로벌 분산 플랫폼 | +| 7 | **UUID v4 완전 난수** | `VARCHAR(36)` | 약 2GB 이상 | 널리 사용되는 방식, 예측이 어려운 식별자 | 랜덤 삽입에 따른 B-Tree 페이지 분할, 인덱스 단편화, 버퍼 풀 효율 저하, 대규모 게시판의 PK로는 부적합할 수 있음 | 보안상 예측 방지가 가장 중요한 분산 데이터 | + +> 인덱스 크기는 자료형, 인덱스 구성, 페이지 여유 공간, 보조 인덱스와 외래키 수에 따라 달라지는 대략적인 값임. + +## 2. 플랫폼에 맞는 3가지 케이스 + +### Case 1. BIGINT PK + 프로젝트 코드 가상 조합 + +#### 구성 + +- DB 저장: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY` +- 화면 및 웹훅: 프로젝트 약어, 채널 약어, `id`를 결합 +- 표시 예시: `ABC-WEB-10492` + +#### 장점 + +- 1,000만 건 기준 PK 인덱스 용량을 약 400MB 수준으로 유지할 수 있음 +- InnoDB Buffer Pool에 데이터와 인덱스를 상주시키기 쉬움 +- 복잡한 채번 테이블이나 별도 락 없이 DB 내장 시퀀스로 생성 가능함 +- 댓글과 히스토리 등 자식 데이터가 많아져도 번호 범위가 충분함 + +#### 단점 + +- 프로젝트별 번호가 1번부터 다시 시작하지 않음 +- 화면과 알림에 프로젝트·채널 코드를 결합하는 로직이 필요함 + +#### 적합한 상황 + +- 단일 DB 중심 구조 +- 운영 오버헤드를 줄이면서 성능과 화면 식별성을 함께 확보해야 하는 경우 +- 전역 번호를 사용해도 업무상 문제가 없는 경우 + +### Case 2. ULID 또는 UUID v7 단독 PK + +#### 구성 + +- DB 저장: `id BINARY(16) PRIMARY KEY` +- 외부 표시: ULID 문자열 또는 접두사를 결합한 형태 +- 표시 예시: `01ARZ3NDEKTSV4RRFFQ69G5FAV`, `fb_01ARZ3...` + +#### 장점 + +- 외부 시스템이나 클라이언트가 중앙 DB에 의존하지 않고 ID를 미리 생성할 수 있음 +- 시간순 정렬이 가능해 UUID v4보다 페이지 분할을 줄일 수 있음 +- 향후 DB 샤딩이나 MSA 분리에 유연함 +- 외부 API와 웹훅의 식별자로 사용하기 좋음 + +#### 단점 + +- BIGINT보다 인덱스 용량이 큼 +- 사람이 구두로 전달하거나 화면에서 빠르게 확인하기 어려움 +- 서버 메모리와 Buffer Pool 요구량이 커질 수 있음 + +#### 적합한 상황 + +- 외부 시스템 연동이 많음 +- 여러 서버와 서비스가 독립적으로 글을 생성함 +- 향후 MSA나 샤딩을 고려함 + +### Case 3. BIGINT PK + 프로젝트·채널별 채번 격리 + +#### 구성 + +- 내부 PK: `id BIGINT UNSIGNED AUTO_INCREMENT` +- 업무 번호: `channel_id INT`, `issue_no INT` +- 유일성: `UNIQUE (channel_id, issue_no)` +- 화면 및 웹훅: `ABC-WEB-1` + +#### 장점 + +- 프로젝트와 채널마다 1번부터 시작하는 직관적인 번호 제공 +- 사용자와 관리자 모두 번호를 쉽게 기억하고 전달할 수 있음 +- 내부 조인과 외래키는 8바이트 정수로 처리해 성능을 유지할 수 있음 + +#### 단점 + +- 새 글 등록 시 채널별 마지막 번호를 안전하게 발급해야 함 +- 동시에 여러 글이 등록되면 채번 락 경합이 발생할 수 있음 +- Redis `INCR` 또는 별도 채번 테이블 등 보조 구조가 필요할 수 있음 + +#### 적합한 상황 + +- Jira와 같이 프로젝트별 번호가 반드시 필요한 경우 +- 사용자 경험과 업무 소통에서 번호의 직관성이 가장 중요한 경우 +- 채널별 등록량이 많지 않고 채번 관리가 가능한 경우 + +## 3. 선택 기준 + +| 우선순위 | 적합한 케이스 | +|---|---| +| 운영 복잡도를 줄이고 성능과 화면 식별성을 함께 확보 | Case 1 | +| 외부 연동, 분산 생성, 향후 MSA 확장성 | Case 2 | +| 프로젝트·채널별 1번부터 시작하는 업무 번호 | Case 3 | + +선택 전에 다음 사항을 확정해야 함. + +1. 화면의 글 ID와 DB의 실제 식별자를 동일하게 표시할지 여부 +2. 전체 프로젝트에서 하나의 번호를 사용할지 여부 +3. 프로젝트 또는 채널마다 번호를 새로 시작할지 여부 +4. 피드백과 이슈가 같은 번호 체계를 사용할지 여부 +5. 외부 Q&A 원본 ID와 관리페이지 ID의 연결 방식 +6. URL과 웹훅에 어떤 ID를 사용할지 여부 +7. 향후 분산 생성 또는 샤딩이 필요한지 여부 diff --git a/docs/관리페이지 md 파일/post-id-strategy-and-performance.v3.previous.md b/docs/관리페이지 md 파일/post-id-strategy-and-performance.v3.previous.md new file mode 100644 index 0000000..354684d --- /dev/null +++ b/docs/관리페이지 md 파일/post-id-strategy-and-performance.v3.previous.md @@ -0,0 +1,435 @@ +# 다중 프로젝트 피드백 플랫폼의 ID 및 성능 설계 + +## 1. 현재 시스템 구조 + +현재 구조는 여러 프로젝트의 작성 서버가 하나의 중앙 관리 서버로 데이터를 보내는 형태임. + +```text +[프로젝트 A 작성 서버] ─┐ +[프로젝트 B 작성 서버] ─┼─ API ─▶ [중앙 관리 서버] ─▶ [중앙 MySQL] +[프로젝트 C 작성 서버] ─┘ +``` + +- 피드백 작성 페이지 서버는 프로젝트마다 별도로 운영함 +- 각 작성 서버가 중앙 관리 API를 호출함 +- 중앙 관리 서버가 피드백을 한곳에 저장하고 관리함 +- 향후 외부 Q&A 서버도 같은 API로 연결할 수 있음 + +이 구조는 여러 서버가 데이터를 만들고 하나의 서버가 모아 관리하는 **다중 발행자·단일 집계 구조**임. + +## 2. 이 구조에서 먼저 해결해야 할 문제 + +### 2.1 재시도에 따른 중복 등록 + +다음과 같은 상황이 발생할 수 있음. + +```text +1. 프로젝트 A가 중앙 API로 글을 전송함 +2. 중앙 서버는 저장했지만 응답이 네트워크 지연으로 늦어짐 +3. 프로젝트 A는 실패로 판단하고 같은 글을 다시 전송함 +4. 중앙 DB가 매번 새 Auto Increment 번호를 발급하면 같은 글이 2건 저장됨 +``` + +따라서 글을 작성하는 서버가 중앙 DB에 보내기 전에 고유 ID를 먼저 만들고, 재시도할 때도 같은 ID를 사용해야 함. + +```text +같은 글 + 같은 ID = 같은 요청으로 판단 +``` + +중앙 API와 DB에는 해당 ID를 기본 키 또는 유일 키로 설정해 중복 저장을 막아야 함. 이를 멱등성(Idempotency)이라고 함. + +### 2.2 중앙 번호 발급에 대한 의존 + +Auto Increment를 사용하면 중앙 DB가 저장하면서 번호를 발급함. + +- 작성 서버는 저장이 끝나야 글 번호를 알 수 있음 +- 여러 프로젝트에서 동시에 등록하면 중앙 DB에 요청이 집중됨 +- 중앙 DB 장애 또는 네트워크 장애가 발생하면 번호 발급도 지연됨 +- 비동기 전송이나 재시도 처리가 복잡해짐 + +### 2.3 프로젝트 구분 + +중앙 관리페이지에는 여러 프로젝트의 글이 함께 보이므로 숫자만 표시하면 어느 프로젝트에서 온 글인지 바로 알기 어려움. + +예를 들어 다음과 같이 구분할 수 있음. + +```text +PRJA_01K8A9V4N5J9F28D5G3H1A2B3C +PRJB_01K8A9V4N5J9F28D5G3H1A2B3D +``` + +단, 화면 표시 ID와 DB에 저장된 식별자를 반드시 같게 유지하려면 위의 전체 문자열을 그대로 저장하는 방식과, DB에는 별도 값을 저장하고 화면에서 조합하는 방식을 구분해야 함. + +## 3. 식별자 선택 시 확인할 기준 + +| 기준 | 확인할 내용 | +|---|---| +| 중복 방지 | 네트워크 재시도에도 같은 글로 인식되는지 | +| 선발급 | 중앙 DB 저장 전에 작성 서버에서 ID를 만들 수 있는지 | +| 분산 생성 | 여러 프로젝트 서버가 동시에 만들어도 충돌하지 않는지 | +| 정렬 성능 | ID가 시간 순으로 생성되어 B-Tree에 유리한지 | +| 저장 크기 | PK와 외래키 인덱스가 과도하게 커지지 않는지 | +| 화면 가독성 | 사람이 읽고 프로젝트를 구분하기 쉬운지 | +| 일관성 | DB ID, API ID, URL ID, 화면 ID가 같은지 | +| 운영 난이도 | 서버별 추가 설정과 관리가 필요한지 | + +## 4. 식별자 체계 7가지 비교 + +| 번호 | 방식 | 저장 스펙 | 인덱스 특성 | 장점 | 단점 | 적합한 환경 | +|---:|---|---|---|---|---|---| +| 1 | `INT AUTO_INCREMENT` | `INT` 4바이트 | 작고 빠름 | 구현이 가장 쉬움, 메모리 효율이 좋음 | 약 21억 개 한계, 중앙 DB 의존, 프로젝트 구분 어려움 | 소규모 단일 서버 | +| 2 | `BIGINT AUTO_INCREMENT` | `BIGINT` 8바이트 | 순차 입력에 유리 | 번호 범위가 매우 큼, 구현이 쉬움 | 중앙 DB에서 번호 발급, 프로젝트 구분 어려움 | 단일 DB 기반 대용량 시스템 | +| 3 | `BIGINT + 프로젝트 코드 조합` | DB는 `BIGINT` | 정수 인덱스 유지 | 저장 성능과 화면 식별성을 함께 확보 | 실제 ID와 화면 조합 ID가 달라질 수 있음 | 중앙 DB 구조와 운영 편의성을 함께 중시하는 경우 | +| 4 | 비즈니스 문자열 직접 저장 | `VARCHAR` | 정수보다 큼 | DB만 봐도 프로젝트와 글을 구분 가능 | 문자열 인덱스 증가, 채번 동시성 문제 | 트래픽이 적고 가독성이 중요한 경우 | +| 5 | `ULID / UUID v7` | `BINARY(16)` 또는 문자열 | 시간 순 입력에 유리 | 서버별 선발급, 분산 생성, 재시도 멱등성에 적합 | 정수보다 크고 사람이 읽기 어려움 | 여러 프로젝트 서버가 API로 전송하는 구조 | +| 6 | Snowflake | `BIGINT` 8바이트 | 시간 순 입력에 유리 | 분산 서버에서 숫자 ID를 선발급할 수 있음 | Worker ID와 시계 동기화 관리 필요 | 매우 높은 쓰기량의 분산 시스템 | +| 7 | UUID v4 | UUID 또는 `VARCHAR(36)` | 랜덤 입력으로 페이지 분할 가능 | 생성이 쉽고 추측이 어려움 | 인덱스가 크고 랜덤 삽입으로 쓰기 효율이 낮아질 수 있음 | 보안상 추측 방지가 최우선인 경우 | + +### 대략적인 저장 크기 + +1,000만 건의 단일 PK 인덱스를 기준으로 한 대략적인 비교임. 실제 크기는 PK 외래키, 보조 인덱스, 페이지 여유 공간에 따라 달라짐. + +| 방식 | 대략적인 PK 인덱스 크기 | +|---|---:| +| `INT` | 약 200MB | +| `BIGINT` | 약 400MB | +| `BINARY(16)` | 약 800MB 이상 | +| `VARCHAR(36)` UUID | 약 1~2GB 이상 | + +## 5. ULID와 Snowflake 비교 + +두 방식 모두 중앙 DB에 저장하기 전에 각 작성 서버에서 ID를 만들 수 있음. + +| 비교 항목 | ULID / UUID v7 | Snowflake | +|---|---|---| +| 저장 크기 | 16바이트 중심 | 8바이트 | +| 시간 순 정렬 | 가능 | 가능 | +| 서버 선발급 | 가능 | 가능 | +| 서버별 설정 | 거의 없음 | Worker ID 필수 | +| 시계 오차 영향 | 상대적으로 낮음 | 민감함 | +| 숫자 가독성 | 낮음 | 숫자지만 긴 숫자임 | +| 운영 난이도 | 낮음 | 높음 | +| 현재 구조 적합성 | 높음 | 조건부 적합 | + +### Snowflake가 동작하는 방식 + +Snowflake는 다음 정보를 조합해 64비트 숫자를 만듦. + +```text +시간 정보 + 서버(Worker) ID + 같은 시간 안의 순번 +``` + +각 프로젝트 서버마다 서로 다른 Worker ID를 배정해야 함. + +```text +프로젝트 A 서버: Worker ID 1 +프로젝트 B 서버: Worker ID 2 +프로젝트 C 서버: Worker ID 3 +``` + +서버가 늘어나거나 컨테이너가 추가될 때마다 번호가 겹치지 않도록 관리해야 함. Worker ID가 중복되면 ID 충돌이 발생할 수 있음. 서버 시간이 뒤로 이동하면 일부 구현에서는 ID 발급을 중단할 수도 있으므로 시간 동기화도 필요함. + +### ULID가 동작하는 방식 + +ULID는 현재 시간과 무작위 값을 조합함. + +```text +시간 정보 + 충돌 가능성이 매우 낮은 무작위 값 +``` + +각 프로젝트 서버에 번호를 따로 배정할 필요가 없음. 여러 서버가 동시에 생성해도 충돌 가능성이 매우 낮고, PK 유일 제약으로 최종 중복도 차단할 수 있음. + +## 6. 현재 구조에 맞는 3가지 케이스 + +### Case 1. BIGINT PK + 프로젝트 코드 별도 표시 + +#### 구성 + +```text +DB ID: 10492 +프로젝트: ABC +채널: WEB +화면 표시: ABC-WEB-10492 +``` + +- DB: `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY` +- 프로젝트와 채널은 별도 컬럼으로 저장 +- 화면과 알림에서 프로젝트 코드와 숫자를 조합 + +#### 장점 + +- 정수 PK와 외래키로 인덱스가 작음 +- 중앙 DB에서 구현하기 쉬움 +- 기존 구조를 가장 적게 변경함 +- Buffer Pool에 데이터와 인덱스를 상주시킬 수 있음 + +#### 주의점 + +- 작성 서버가 글을 전송하기 전에 중앙 DB의 ID를 알 수 없음 +- 네트워크 재시도 중복을 막으려면 별도의 Idempotency Key가 필요함 +- 화면의 `ABC-WEB-10492`와 DB의 `10492`가 달라질 수 있음 + +#### 적합한 경우 + +- 중앙 DB 저장 완료 후 ID를 받아도 되는 경우 +- 기존 시스템 변경을 최소화해야 하는 경우 +- 화면 ID와 실제 DB ID가 반드시 같을 필요가 없는 경우 + +### Case 2. ULID 또는 UUID v7을 실제 글 ID로 사용 + +#### 구성 + +화면 ID와 API ID를 동일하게 유지하려면 전체 식별자를 DB에 그대로 저장함. + +```text +DB ID / API ID / 화면 ID: +PRJA_01K8A9V4N5J9F28D5G3H1A2B3C +``` + +가능한 저장 방식은 두 가지임. + +| 방식 | 설명 | +|---|---| +| 전체 문자열 저장 | 접두사와 ULID를 `VARCHAR`에 저장해 화면 ID와 DB ID를 동일하게 유지함 | +| 분리 저장 | 프로젝트 코드와 ULID를 컬럼으로 나누고, 화면에서 다시 조합함 | + +이번 요구처럼 글 ID와 표시 ID를 일치시키려면 **전체 문자열 저장**이 더 명확함. 이 경우 문자열 PK의 인덱스 크기를 감수해야 함. + +#### 등록 흐름 + +```text +1. 프로젝트 작성 서버가 ID를 먼저 생성함 +2. 글 데이터와 ID를 중앙 API로 전송함 +3. 중앙 DB가 ID를 PK 또는 UNIQUE KEY로 확인함 +4. 같은 ID의 재요청이면 중복 저장하지 않음 +5. 중앙 관리페이지는 전달받은 ID를 그대로 표시함 +``` + +#### 장점 + +- 프로젝트별 작성 서버가 독립적으로 ID를 선발급할 수 있음 +- 타임아웃 후 재시도해도 같은 글로 처리할 수 있음 +- 중앙 번호 발급을 기다리지 않아 비동기 전송이 쉬움 +- 시간 정보가 앞에 있어 B-Tree 인덱스가 UUID v4보다 유리함 +- 별도의 Worker ID 관리가 필요 없음 + +#### 단점 + +- `BIGINT`보다 PK와 외래키 인덱스가 큼 +- 사람이 숫자 하나로 기억하거나 구두 전달하기 어려움 +- 프로젝트 접두사를 포함하면 ID가 더 길어짐 + +#### 적합한 경우 + +- 프로젝트별 작성 서버가 완전히 분리되어 있음 +- 중앙 API로 여러 서버의 데이터를 수집함 +- 글 ID와 화면 표시 ID를 일치시켜야 함 +- 재시도 중복과 비동기 전송을 안정적으로 처리해야 함 + +### Case 3. Snowflake 숫자 ID + 프로젝트 정보 별도 저장 + +#### 구성 + +```text +DB ID: 178293849182394880 +프로젝트: ABC +화면 표시: 178293849182394880 +``` + +- `id BIGINT UNSIGNED PRIMARY KEY` +- 프로젝트 코드는 별도 컬럼으로 저장 +- 각 작성 서버에 고유 Worker ID를 배정함 +- 모든 서버의 시계를 동기화함 + +#### 장점 + +- 화면과 DB에서 동일한 숫자 ID를 사용할 수 있음 +- ULID보다 인덱스와 외래키 크기가 작음 +- 여러 서버에서 중앙 DB 의존 없이 ID를 선발급할 수 있음 +- 시간 순 정렬이 가능함 + +#### 단점 + +- 프로젝트와 서버별 Worker ID 관리가 필요함 +- 오토스케일링과 서버 추가 시 번호 배정 정책이 필요함 +- Worker ID 중복 여부를 관리해야 함 +- 시계가 뒤로 이동하는 상황에 대한 처리 필요 +- 현재 등록량에서는 운영 복잡도가 성능 이점보다 클 수 있음 + +#### 적합한 경우 + +- 숫자 ID가 반드시 필요함 +- 초당 수천~수만 건의 등록이 발생함 +- 서버와 Worker ID를 중앙에서 안정적으로 관리할 수 있음 + +## 7. 선택 결론 + +현재 구조에서는 **Case 2: ULID 또는 UUID v7을 실제 글 ID로 사용하는 방식**이 가장 자연스러움. + +이유는 다음과 같음. + +1. 프로젝트별 작성 서버가 중앙 DB에 의존하지 않고 ID를 먼저 만들 수 있음 +2. 네트워크 타임아웃과 재시도에 따른 중복 등록을 막기 쉬움 +3. 프로젝트 서버가 늘어나도 Worker ID를 따로 배정할 필요가 없음 +4. 시간 순 ID라 UUID v4보다 인덱스 페이지 분할에 유리함 +5. 전체 ID를 DB에 그대로 저장하면 화면 ID와 API ID를 일치시킬 수 있음 + +다만 숫자 ID가 반드시 필요하다면 Case 3을 선택할 수 있음. 이 경우 ULID보다 저장 공간은 작지만 Worker ID와 서버 시계 관리가 필수임. + +## 8. 일상적인 비유 + +### Snowflake + +각 지점에 번호표 기계를 설치하고 지점 번호를 미리 배정하는 방식임. + +- 강남점은 1번 +- 판교점은 2번 +- 여의도점은 3번 + +지점 번호가 겹치지 않으면 아주 작은 숫자 번호표를 빠르게 만들 수 있음. 하지만 지점이 늘어날 때마다 번호를 관리해야 하고, 지점 시계가 맞지 않으면 번호표 발급에 문제가 생길 수 있음. + +### ULID + +각 지점이 별도 등록 없이 앱을 실행해 바로 고유 번호표를 만드는 방식임. + +- 지점 번호를 따로 배정하지 않아도 됨 +- 여러 지점에서 동시에 만들어도 충돌 가능성이 매우 낮음 +- 네트워크가 끊겨 같은 번호표를 다시 보내도 중앙에서 중복을 확인할 수 있음 +- 번호표가 시간 순으로 만들어져 정리하기 쉬움 + +현재처럼 프로젝트별 작성 서버가 여러 곳에 나뉘어 있다면, 관리해야 할 설정이 적은 ULID가 운영 측면에서 유리함. + +## 9. 피드백 테이블 인덱스 설계 + +### 주요 조회 조건 + +```sql +WHERE channel_id = ? + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20 +``` + +### 기본 후보 + +```sql +CREATE INDEX idx_feedbacks_channel_active_list +ON feedbacks (channel_id, deleted_at, id DESC); +``` + +이 인덱스는 채널, 삭제 여부, 최신순 정렬을 함께 고려한 것임. + +`deleted_at`을 포함한 이유는 소프트 삭제된 글을 목록에서 제외하는 조건까지 인덱스 탐색 범위에 포함하기 위해서임. 다만 삭제된 글이 거의 없으면 다음 인덱스가 더 작고 효율적일 수도 있음. + +```sql +CREATE INDEX idx_feedbacks_channel_list +ON feedbacks (channel_id, id DESC); +``` + +둘 중 어떤 방식이 좋은지는 삭제 데이터 비율과 실제 실행 계획으로 확인해야 함. + +### 미확인 피드백 조회 + +```sql +CREATE INDEX idx_feedbacks_unread +ON feedbacks (channel_id, admin_first_read_at, deleted_at); +``` + +### JSON 내부 값 조회 + +`data` 내부의 `category`를 자주 검색한다면 생성 컬럼과 인덱스를 구성할 수 있음. + +```sql +ALTER TABLE feedbacks +ADD COLUMN category VARCHAR(50) +GENERATED ALWAYS AS (data->>'$.category') VIRTUAL; +``` + +```sql +CREATE INDEX idx_feedbacks_channel_category +ON feedbacks (channel_id, category, id DESC); +``` + +## 10. 5만 건 기준 메모리와 캐시 + +현재 523행의 데이터 용량이 약 `528.0 KiB`라면 다음과 같이 추정할 수 있음. + +| 항목 | 예상 크기 | +|---|---:| +| 5만 건 데이터 | 약 50~70MB | +| 복합 인덱스 2~3개 | 약 4~6MB | +| 데이터와 인덱스 합계 | 약 80MB 미만 | + +실제 크기는 JSON 또는 TEXT 데이터 길이, 인덱스 자료형, 보조 인덱스 수에 따라 달라짐. + +### InnoDB Buffer Pool + +```text +[MySQL 데이터와 인덱스] + │ + ▼ +[InnoDB Buffer Pool 메모리 상주] + │ + ▼ +[반복 조회 시 디스크 접근 감소] +``` + +Buffer Pool이 데이터와 인덱스보다 충분히 크면 자주 조회되는 페이지가 메모리에 상주할 수 있음. 다만 “항상 100% 상주” 또는 “항상 1ms 이하”라고 단정할 수는 없으며, 서버 메모리와 실행 계획을 확인해야 함. + +### Redis 애플리케이션 캐시 + +```text +[클라이언트] + │ + ▼ +[Redis 캐시] ── HIT ──▶ 캐시 결과 반환 + │ + MISS + ▼ +[중앙 API] ──▶ [MySQL] ──▶ Redis 갱신 +``` + +우선 캐싱할 대상: + +- 프로젝트·채널별 최신 피드백 목록 +- 각 목록의 첫 페이지 +- 전체 피드백 수와 상태별 개수 +- 답변 대기·미확인 요약 정보 + +캐시 키 예시: + +```text +feedback:project:{project_id}:channel:{channel_id}:page:1 +``` + +새 글 등록, 수정, 삭제가 발생하면 관련 캐시를 삭제하거나 갱신해야 함. + +| 방식 | 설명 | +|---|---| +| 즉시 삭제 | 변경 시 관련 캐시를 바로 삭제함 | +| 짧은 TTL | 30초~1분 후 자동 만료함 | +| 조합 | 첫 페이지는 즉시 삭제하고 나머지는 TTL을 사용함 | + +## 11. 성능 점검 방법 + +인덱스를 적용하기 전에 실제 실행 계획을 확인해야 함. + +```sql +EXPLAIN ANALYZE +SELECT * +FROM feedbacks +WHERE channel_id = 1 + AND deleted_at IS NULL +ORDER BY id DESC +LIMIT 20; +``` + +다음 항목을 확인함. + +- 예상한 인덱스가 선택되는지 +- `key`에 복합 인덱스가 표시되는지 +- `rows`가 과도하게 많지 않은지 +- 전체 테이블 스캔이 발생하지 않는지 +- `Using filesort` 또는 `Using temporary`가 발생하는지 + +인덱스 적용 전후의 실행 시간, 읽은 행 수, DB CPU와 디스크 I/O를 함께 비교해야 함. diff --git a/docs/관리페이지 md 파일/post-id-strategy-options.md b/docs/관리페이지 md 파일/post-id-strategy-options.md new file mode 100644 index 0000000..7abc2c0 --- /dev/null +++ b/docs/관리페이지 md 파일/post-id-strategy-options.md @@ -0,0 +1,225 @@ +# 게시판 글 ID 설계 방식 + +게시판 글 ID(Primary Key 및 식별자)는 조회 성능, 보안, 분산 환경 지원 여부, URL 가독성 등에 따라 여러 방식으로 설계할 수 있음. + +## 1. 자동 증가 정수형 + +### Auto Increment / Sequence + +MySQL의 `AUTO_INCREMENT`, PostgreSQL의 `BIGSERIAL` 또는 `IDENTITY`를 사용하는 전통적인 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `1`, `2`, `3`, `4`, ... | +| 저장 방식 | `INT` 또는 `BIGINT` | +| 구현 난이도 | 가장 낮음 | +| 인덱스 성능 | 매우 좋음 | + +### 장점 + +- 구현이 가장 쉽고 직관적임 +- 저장 용량이 적음 +- B-Tree 인덱스 정렬 성능이 좋음 +- 최신 글 순서대로 자동 정렬하기 쉬움 + +### 단점 + +- 다음 글 번호를 쉽게 추측할 수 있음 +- 크롤링이나 글 수량 파악에 취약할 수 있음 +- 분산 DB 또는 멀티 마스터 환경에서 충돌 없이 발급하기 어려움 + +### 적합한 사용처 + +- 사내 인트라넷 +- 관리자 전용 게시판 +- 단일 DB 인스턴스 환경 + +## 2. UUID v4 + +### 완전 랜덤 128비트 식별자 + +전 세계에서 고유한 값을 무작위로 생성하는 표준 방식. + +| 구분 | 내용 | +|---|---| +| 형태 | `c9a646d3-9c61-4cc9-bc19-482386a37365` | +| 저장 방식 | UUID 또는 `CHAR(36)` 등 | +| 생성 방식 | 완전 랜덤 | +| 분산 환경 | 지원 | + +### 장점 + +- 중복 가능성이 매우 낮음 +- 중앙 조율 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 +- 분산 서버 환경에 적합함 + +### 단점 + +- 36자 문자열이라 URL이 길고 복잡함 +- 완전 랜덤 값이라 B-Tree 인덱스 페이지 분할이 자주 발생할 수 있음 +- 대량 입력 환경에서 정수형보다 쓰기 성능이 낮을 수 있음 + +### 적합한 사용처 + +- 외부 API 통신 식별자 +- 추측 방지가 중요한 비즈니스 데이터 +- 분산 시스템 + +## 3. 시간 순 정렬 가능 고유 식별자 + +### UUID v7 / ULID + +UUID의 고유성과 Auto Increment의 시간 정렬 장점을 결합한 방식. + +| 방식 | 예시 | +|---|---| +| UUID v7 | `018d3b2f-7a54-7c3a-8b1e-9d2a4e5b6c7d` | +| ULID | `01ARZ3NDEKTSV4RRFFQ69G5FAV` | + +### 장점 + +- 생성된 시간 순으로 정렬 가능함 +- B-Tree 인덱스에 순차적으로 적재되어 쓰기 성능이 좋음 +- 분산 환경에서 충돌 없이 생성 가능함 +- 다음 ID를 추측하기 어려움 + +### 단점 + +- 정수형보다 저장 공간을 많이 사용함 +- UUID v4보다 생성 규칙이 복잡함 +- ID만 보고 생성 시점을 일부 추정할 수 있음 + +### 적합한 사용처 + +- 대용량 트래픽 게시판 +- 분산 서비스 +- 최신 웹 애플리케이션 + +## 4. 트위터 스노우플레이크 + +### Snowflake + +대량의 데이터를 분산 환경에서 생성하기 위해 고안된 64비트 정수 기반 방식. + +### 기본 구조 + +```text +타임스탬프 41비트 + 데이터센터/머신 ID 10비트 + 시퀀스 번호 12비트 +``` + +### 예시 + +```text +1542893478912349184 +``` + +### 장점 + +- 64비트 정수라 인덱싱 성능이 좋음 +- 시간 순 정렬이 가능함 +- 분산 서버 간 ID 충돌을 방지할 수 있음 +- 대규모 시스템에서 높은 처리량을 지원함 + +### 단점 + +- 별도의 ID 생성기 또는 Worker 관리가 필요함 +- 데이터센터 및 머신 ID 관리가 필요함 +- JavaScript에서 일반 숫자로 처리하면 정밀도 손실이 발생할 수 있음 +- Auto Increment보다 운영 복잡도가 높음 + +### 적합한 사용처 + +- 대규모 분산 SNS +- 초대형 커뮤니티 +- 초당 수천 건 이상의 글이 등록되는 서비스 + +## 5. 해시 기반 난독화 + +### Sqids / Hashids + +DB에는 정수형 ID를 저장하고, URL이나 외부 화면에서는 별도의 문자열로 변환하여 표시하는 방식. + +| 구분 | 예시 | +|---|---| +| 내부 DB ID | `1204` | +| 외부 URL ID | `b9Xq` | + +### 장점 + +- DB에서는 정수형 인덱스 성능을 유지할 수 있음 +- 짧고 깔끔한 URL을 만들 수 있음 +- 외부에서 원래 순번을 바로 유추하기 어려움 +- 기존 정수형 DB 구조를 유지하면서 URL만 변경할 수 있음 + +### 단점 + +- 내부 ID와 외부 표시 ID가 달라짐 +- 암호화가 아니라 인코딩에 가까움 +- 시크릿 솔트가 노출되면 원래 정수값을 역산할 수 있음 +- 운영자가 같은 글을 서로 다른 ID로 인식할 수 있음 + +### 적합한 사용처 + +- 외부 URL을 짧게 만들고 싶은 서비스 +- 기존 정수형 DB를 유지하면서 외부 노출만 바꾸려는 서비스 +- YouTube와 같은 짧은 식별자가 필요한 서비스 + +## 6. 비즈니스 접두사 결합형 + +### Stripe 스타일 ID + +글의 종류나 도메인을 식별할 수 있도록 ID 앞에 접두사를 붙이는 방식. + +### 예시 + +```text +post_01ARZ3NDEKTSV4RRFFQ69G5FAV +qna_01ARZ3NDEKTSV4RRFFQ69G5FAV +fb_202609_a8f3d +``` + +### 장점 + +- ID만 보고도 데이터 종류를 구분할 수 있음 +- 로그나 알림에서 식별이 쉬움 +- API 중심 서비스에 적합함 +- 디버깅과 모니터링이 편리함 + +### 단점 + +- 숫자만 사용하는 방식보다 ID가 길어짐 +- 글 종류가 변경되면 접두사 정책도 고려해야 함 +- 단순한 게시판에서는 다소 복잡할 수 있음 + +### 적합한 사용처 + +- API 중심 B2B SaaS +- 여러 유형의 게시물이 함께 존재하는 시스템 +- 로그와 웹훅에서 데이터 유형을 즉시 구분해야 하는 서비스 + +## 방식별 비교 + +| 방식 | 저장 타입 | URL 길이 | 분산 환경 | 인덱스 성능 | 추측 방지 | 적합한 상황 | +|---|---|---:|:---:|:---:|:---:|---| +| Auto Increment | `INT` / `BIGINT` | 매우 짧음 | 낮음 | 매우 좋음 | 낮음 | 내부 관리용, 소규모 서비스 | +| UUID v4 | UUID / `CHAR` | 김 | 높음 | 보통 | 높음 | 보안이 중요한 데이터 | +| UUID v7 / ULID | UUID / 16바이트 등 | 중간 | 높음 | 좋음 | 높음 | 대용량·분산 웹 서비스 | +| Snowflake | `BIGINT` | 짧음 | 높음 | 매우 좋음 | 보통 | 대규모 분산 시스템 | +| Sqids / Hashids | DB는 정수형 | 외부는 짧음 | 낮음 | 매우 좋음 | 보통 | URL 난독화가 필요한 경우 | +| 접두사 결합형 | 문자열 또는 조합형 | 중간~김 | 방식에 따라 다름 | 방식에 따라 다름 | 방식에 따라 다름 | API·로그 식별이 중요한 경우 | + +## 검토할 때 확인할 기준 + +ID 방식을 선택할 때는 다음 항목을 함께 확인해야 함. + +1. 화면에 표시되는 ID와 DB의 실제 ID를 같게 유지할 것인지 +2. 프로젝트마다 번호를 새로 시작할 것인지 +3. 프로젝트를 넘어선 전체 글 번호가 필요한지 +4. 피드백과 이슈가 같은 번호 체계를 사용할 것인지 +5. 외부 Q&A 원본 ID와 관리페이지 ID를 어떻게 연결할 것인지 +6. URL에서 ID를 직접 노출할 것인지 +7. 단일 서버인지 분산 서버인지 +8. 향후 글 수가 얼마나 증가할지 + +특히 내부 DB ID와 화면 표시 ID를 별도로 운영하면, 운영자가 같은 글을 서로 다른 번호로 인식할 수 있으므로 표시 정책을 먼저 확정해야 함. diff --git a/docs/관리페이지 md 파일/qna-platform-prototype-2-feedback-tasks.md b/docs/관리페이지 md 파일/qna-platform-prototype-2-feedback-tasks.md new file mode 100644 index 0000000..d9652d0 --- /dev/null +++ b/docs/관리페이지 md 파일/qna-platform-prototype-2-feedback-tasks.md @@ -0,0 +1,293 @@ +# Q&A 관리자 콘솔 피드백 개선 작업 정리 + +대상 화면: [`qna-platform-prototype-2.html`](../qna-platform-prototype-2.html) + +## 1. 작업 목표 + +관리자 콘솔에서 피드백을 접수한 뒤 담당자 배정, 내부 협업, 기존 이슈 연결 또는 신규 Gitea 이슈 생성, 고객 답변 및 처리 완료까지 한 흐름으로 관리할 수 있도록 개선한다. + +이번 작업은 화면 목업의 방향을 기준으로 하되, 현재 백엔드에 이미 존재하는 티켓·댓글·첨부파일 구조를 최대한 재사용한다. + +## 2. 현재 프로토타입에 이미 반영된 내용 + +| 영역 | 현재 상태 | 후속 보완 | +| --- | --- | --- | +| 피드백 목록 Status | 목록에 `Status` 컬럼이 존재함 | 실제 저장/필터/상태 변경 동작 연결 | +| 상세 처리 정보 | 상태·담당자·우선순위 선택 UI가 있음 | 프로젝트별 매니저 목록 조회 및 저장 | +| 내부 메모 | 상세창에 내부 메모 textarea가 있음 | 관리자 전용 저장, 작성자·작성일 이력화 | +| 이슈 연결 | 기존 이슈 추천, 이슈 검색 입력, 신규 이슈 생성 버튼이 있음 | 기존 이슈 검색 결과·연결 피드백 조회 및 Gitea 연동 | +| 첨부파일 | 상세창에 첨부 영역 placeholder가 있음 | 업로드/조회/다운로드 및 R2 저장 | +| 운영 지표 | 답변 대기, 이슈 연결률, 평균 처리 시간 등이 일부 표시됨 | 오늘 신규 피드백·처리 대기·평균 처리 시간 기준으로 재정의 | +| 상세 상단 메타 | ID, Created, Updated가 보임 | Issue까지 포함해 한 줄 메타 정보로 통일 | + +## 3. 요구사항별 작업 목록 + +### A. Gitea 이슈 생성 payload 확장 + +- [x] Gitea 이슈 생성 시 다음 정보를 전송한다. + - 피드백 ID 및 관리자 콘솔 상세 URL + - 피드백 제목 + - 피드백 description/원문 + - 첨부파일 URL 목록 + - 제품/프로젝트 정보 + - 생성일 및 내부 이슈 피드백 수 +- [ ] 첨부파일 URL은 인증이 필요한 경우에도 개발자가 접근할 수 있는 방식으로 제공한다. + - 권장: 만료시간이 있는 presigned URL 또는 Gitea 전달용 안전한 proxy URL + - 원본 파일을 Gitea 이슈 본문에 직접 삽입하지 않고 링크와 파일명·용량·MIME type을 함께 전송 +- [x] Gitea API 실패·재시도·중복 생성 방지를 정의한다. + - 외부 이슈 ID, 이슈 URL, 외부 상태, 동기화 상태, 마지막 동기화 오류를 저장 + - 이미 외부 이슈가 연결된 내부 이슈는 신규 생성 API를 다시 호출하지 않는다. + - 재시도 시 중복 생성 방지는 후속 보완 대상이다. +- [x] 신규 생성 이후 Gitea 이슈 번호/URL을 상세창과 목록의 Issue 항목에 표시한다. + +구현 완료 범위: +- 신규 Gitea 이슈 생성 본문에 프로젝트, 내부 이슈, 연결된 피드백의 ID·상세 URL·제목·description·첨부 URL을 포함한다. +- 외부 이슈 URL·상태·동기화 상태·오류·동기화 시각을 내부 이슈에 저장한다. +- 이미 외부 이슈가 연결된 내부 이슈는 생성 API를 호출하지 않는다. + +### B. 프로젝트별 매니저 담당자 배정 + +- [x] 피드백 상세창의 담당자 선택 목록을 현재 프로젝트에 권한이 있는 매니저 목록으로 교체한다. +- [x] `미지정` 해제 및 담당자 변경을 저장한다. (변경 이력은 후속 단계에서 추가) +- [ ] 목록에서 담당자 기준 필터를 제공한다. + - 전체 + - 미지정 + - 내 담당 + - 특정 매니저 +- [x] 담당자 목록이 비어 있거나 조회에 실패할 때 안내 문구를 제공한다. (재시도 동작은 후속 단계에서 추가) +- [x] 현재 백엔드의 `current_assignee_id`, `current_assignee_tenant_id`를 재사용하고, 프로젝트/워크스페이스 권한 검증을 적용한다. + +구현 완료 범위: +- Secretary API에 프로젝트 관리자 후보 조회와 담당자 지정/해제 API를 추가했다. +- 후보자는 해당 workspace의 `PROJECT_MANAGER`와 전체 관리자이며, 다른 프로젝트 관리자는 지정할 수 없다. +- 현재 로그인한 관리자는 같은 프로젝트의 다른 관리자도 지정할 수 있다. +- 웹 피드백 상세창에서 담당자 선택 및 미지정 해제를 지원한다. + +### C. 관리자 내부 메모 + +- [x] 피드백 상세창에 관리자 전용 내부 메모 입력·저장 영역을 제공한다. +- [x] 내부 메모는 고객에게 노출되는 답변 댓글과 분리한다. +- [x] 메모 목록에 작성자, 작성일, 수정일을 표시한다. +- [x] 메모 수정 및 삭제 권한을 정의한다. +- [x] 기존 `ticket_comments.is_internal` 구조를 재사용하고, `comment_type=INTERNAL_MEMO`로 전용 메모를 구분한다. + - 별도 테이블을 만들지 않아 기존 댓글·첨부파일·권한 구조와 호환된다. + +구현 완료 범위: +- `GET/POST /api/tickets/{ticketId}/internal-memos` +- `PUT/DELETE /api/tickets/{ticketId}/internal-memos/{memoId}` +- 프로젝트 관리자 이상만 조회·작성·수정·삭제 가능 +- 피드백 상세창에 내부 메모 목록과 입력·수정·삭제 UI 추가 + +### D. 기존 Gitea 이슈 연결 및 동일 이슈 피드백 조회 + +- [x] 신규 이슈 생성 외에 기존 Gitea 이슈를 검색하고 연결할 수 있게 한다. + - 이슈 번호 + - 이슈 제목 + - 키워드 + - 이슈 상태 +- [ ] 상세창에서 현재 연결된 이슈 정보와 연결 해제 동작을 제공한다. +- [x] 연결된 이슈의 동일 피드백 목록을 조회한다. + - 피드백 ID, 제목, 상태, 담당자, 생성일 + - 현재 피드백을 목록에서 구분 + - 프로젝트 범위 내 조회를 기본으로 함 +- [ ] 한 피드백에 연결할 수 있는 외부 이슈의 cardinality를 결정한다. + - 1차 구현 권장: 피드백 1건당 대표 Gitea 이슈 1건 + - 필요 시 이슈-피드백 다대다 확장 가능하도록 매핑 테이블을 고려 +- [x] 기존 이슈 연결 시 신규 생성으로 처리되지 않도록 외부 이슈 ID 기반으로 저장한다. + +구현 완료 범위: +- Gitea 저장소의 기존 이슈를 번호·키워드로 검색한다. +- 내부 이슈 상세창에서 검색 결과를 선택해 기존 Gitea 이슈 번호를 연결한다. +- 연결 시 `external_issue_id`에 Gitea 이슈 번호를 저장해 신규 생성과 구분한다. +- 동일 이슈 피드백 조회, 연결 해제, 연결된 이슈 메타 정보 표시는 후속 작업이다. + +### E. 피드백 탭 상단 운영 지표 + +- [ ] 피드백 탭 상단에 다음 지표를 표시한다. + - 오늘 등록된 신규 피드백 건수 + - 처리 대기 건수 + - 평균 처리 시간 +- [ ] 기준 시간대는 서비스 설정 또는 프로젝트 설정의 timezone을 사용하고, 기본값은 `Asia/Seoul`로 확인한다. +- [ ] 지표의 기준을 명확히 한다. + - 오늘 신규: 오늘 `created_at`이 시작 시각 이후인 피드백 + - 처리 대기: 완료/종료 상태가 아닌 피드백 + - 평균 처리 시간: 생성 시각부터 최초 완료 시각까지의 평균 +- [ ] 데이터가 없는 경우 평균 처리 시간을 `-`로 표시한다. +- [ ] 지표와 목록의 필터 기준이 어긋나지 않도록 동일한 프로젝트·기간 범위를 사용한다. + +### F. 피드백 목록 Status 표시 및 필터 + +- [ ] 목록 항목에 피드백 자체의 처리 상태를 표시한다. +- [ ] 상태 배지의 색상과 명칭을 통일한다. +- [ ] 최소 상태 후보: + - `신규` + - `검토 중` + - `답변 준비` + - `답변 완료` + - `종료` +- [ ] 상태 변경 시 변경 시각과 변경자를 이력에 남긴다. +- [ ] 상태 필터, 정렬, 페이지네이션이 서버 조회 기준으로 동작하도록 한다. + +### G. 이슈 상태와 피드백 처리 상태 분리 + +- [ ] 이슈 연결 여부/외부 이슈 상태와 피드백 처리 상태를 별도 필드로 관리한다. + - `feedback_status`: 고객 피드백 처리 상태 + - `issue_link_status`: 이슈 미연결/기존 이슈 연결/신규 이슈 생성 등 연결 상태 + - `external_issue_status`: Gitea 이슈의 open/closed 등 외부 상태(필요한 경우) +- [ ] 고객 답변 댓글 등록 후 이슈를 생성하지 않고도 피드백을 완료 처리할 수 있게 한다. +- [ ] 이슈를 연결했다고 해서 피드백이 자동 완료되지 않도록 한다. +- [ ] 상태 전이 규칙과 완료 조건을 정의한다. + - 예: `신규 → 검토 중 → 답변 준비 → 답변 완료 → 종료` + - 예: `검토 중 → 이슈 연결`은 피드백 상태가 아니라 연결 상태에만 반영 +- [ ] 기존 `status_code`가 지원 요청 lifecycle인지 피드백 처리 상태인지 확인한 후, 의미가 다르면 마이그레이션으로 분리한다. + +### H. 모든 바이너리의 R2 저장 + +- [ ] 피드백 본문 첨부파일, 고객 댓글 이미지, 관리자 댓글 이미지 등 모든 바이너리 저장 대상을 목록화한다. +- [ ] 로컬 파일 저장을 R2 object storage 저장으로 전환한다. +- [ ] 첨부파일 메타데이터는 DB에 저장한다. + - storage provider + - bucket + - object key + - 원본 파일명 + - MIME type + - 용량 + - SHA-256 checksum + - 업로더 및 생성일 +- [ ] 브라우저에는 직접 public URL을 노출하지 않고, 권한 검증 후 presigned download URL 또는 streaming endpoint를 제공한다. +- [ ] 기존 LOCAL 첨부파일의 처리 방식을 정한다. + - 마이그레이션 기간에는 LOCAL 조회를 유지하고 신규 파일부터 R2 저장 + - 이후 기존 파일을 R2로 이관하고 DB provider/key를 갱신 +- [ ] 업로드 실패 시 DB 메타데이터와 R2 object가 함께 정리되도록 보상 처리를 구현한다. +- [ ] 파일 크기, 확장자, MIME type, 이미지 첨부 제한, 바이러스 검사 여부를 기존 정책과 함께 검토한다. +- [ ] 현재 `Attachment.storage_provider` 기본값이 `LOCAL`이고 `UPLOAD_ROOT_DIR`에 파일을 쓰는 구조이므로, R2 설정값과 storage adapter를 추가한다. + +### I. 상세창 최상위 글 메타 정보 한 줄 표시 + +- [ ] 상세창 최상단에 다음 항목을 한 줄의 메타 정보로 표시한다. + - `ID` + - `Created` + - `Updated` + - `Issue` +- [ ] Issue가 없을 때는 `미연결`로 표시한다. +- [ ] Issue가 연결된 경우 Gitea issue key와 클릭 가능한 URL을 표시한다. +- [ ] 좁은 화면에서는 줄바꿈 가능한 반응형 레이아웃으로 전환한다. + +## 4. 권장 데이터/API 변경안 + +### 피드백/티켓 + +기존 `support_tickets`를 재사용할 수 있는지 먼저 확인한다. 단, 아래 필드는 의미를 명확히 분리해야 한다. + +```text +feedback_status # 피드백 자체 처리 상태 +issue_link_status # 외부 이슈 연결 상태 +external_issue_id # Gitea issue number 또는 provider issue id +external_issue_key # 예: PROJECT-123 +external_issue_url +external_issue_status +assigned_manager_id +assigned_manager_tenant_id +first_response_at +resolved_at +closed_at +``` + +현재 모델의 `current_assignee_id`, `current_assignee_tenant_id`, `status_code`, `issue_link_status`, `title`, `description`, `created_at`, `updated_at`를 우선 매핑 대상으로 삼는다. 실제 컬럼명을 바꾸기 전 기존 API 소비처와 마이그레이션 호환성을 확인한다. + +### 내부 메모 + +```text +GET /api/admin/projects/{projectId}/feedbacks/{feedbackId}/internal-memos +POST /api/admin/projects/{projectId}/feedbacks/{feedbackId}/internal-memos +PUT /api/admin/projects/{projectId}/feedbacks/{feedbackId}/internal-memos/{memoId} +``` + +기존 ticket comment API에서 `is_internal=true`를 지원하는 경우 별도 API를 만들지 않고 관리자용 wrapper로 제공할 수 있다. + +### 담당자 + +```text +GET /api/admin/projects/{projectId}/managers +PATCH /api/admin/projects/{projectId}/feedbacks/{feedbackId}/assignee +``` + +응답에는 선택 UI에 필요한 `id`, `tenantId`, `name`, `email` 또는 표시용 식별자를 포함한다. + +### 이슈 검색/연결 + +```text +GET /api/admin/projects/{projectId}/gitea/issues/search?q=... +POST /api/admin/projects/{projectId}/feedbacks/{feedbackId}/issue-link +DELETE /api/admin/projects/{projectId}/feedbacks/{feedbackId}/issue-link +GET /api/admin/projects/{projectId}/gitea/issues/{issueId}/feedbacks +``` + +### 운영 지표 + +```text +GET /api/admin/projects/{projectId}/feedbacks/metrics?from=...&to=... +``` + +응답 예시: + +```json +{ + "todayNewCount": 0, + "pendingCount": 0, + "averageResolutionMinutes": 0, + "timezone": "Asia/Seoul" +} +``` + +## 5. 구현 순서 + +1. [x] 상태 모델 확정: 피드백 상태와 이슈 연결/외부 이슈 상태 분리 +2. [x] 프로젝트별 매니저 조회 및 담당자 저장 +3. [x] 내부 메모 저장/조회 및 관리자 권한 처리 +4. [x] 기존 이슈 검색·연결 및 동일 이슈 피드백 조회 (연결 해제는 후속) +5. [x] Gitea 신규 이슈 생성 payload와 동기화 상태 구현 +6. [x] 첨부파일 storage adapter와 R2 저장/다운로드 구현 +7. [x] 운영 지표 API 및 상단 카드 구현 +8. [x] 목록 Status, 상세 메타 한 줄, 상세 처리 액션 UI 연결 +9. [ ] 기존 LOCAL 첨부파일 이관 및 회귀 테스트 + +## 6. 완료 기준 + +- [x] 관리자가 피드백 상세에서 현재 프로젝트의 매니저를 선택하고 저장할 수 있다. +- [x] 관리자 내부 메모가 고객 답변과 분리되어 저장·조회된다. +- [ ] 기존 Gitea 이슈를 연결하면 이슈에 연결된 다른 피드백을 조회할 수 있다. +- [ ] 신규 Gitea 이슈 생성 시 제목, description, 첨부파일 URL, 피드백 메타데이터가 전달된다. +- [ ] 이슈를 만들지 않고 고객 답변만 등록한 피드백도 별도 처리 상태로 완료할 수 있다. +- [x] 목록과 상세에 피드백 처리 상태가 표시되고 변경된다. +- [x] 피드백 탭 상단에서 오늘 신규, 처리 대기, 평균 처리 시간을 확인할 수 있다. +- [x] 신규 및 댓글 이미지 첨부파일이 R2에 저장되고 권한 있는 사용자만 다운로드할 수 있다. +- [x] 상세창 최상단에 ID/Created/Updated/Issue가 한 줄 메타 정보로 표시된다. +- [ ] 권한 없는 관리자는 담당자·내부 메모·상태·이슈 연결을 변경할 수 없다. + +## 7. 확인이 필요한 결정사항 + +- [ ] Gitea가 프로젝트별로 동일 인스턴스인지, 프로젝트별 repository가 다른지 +- [ ] Gitea API 인증 방식과 issue 생성/검색 endpoint +- [ ] Gitea 이슈 본문에서 첨부파일 URL을 접근할 수 있어야 하는 네트워크·인증 조건 +- [ ] 피드백 1건에 대표 이슈 1건만 허용할지, 여러 이슈 연결을 허용할지 +- [ ] `답변 완료`와 `종료`를 별도 상태로 운영할지 +- [ ] 평균 처리 시간의 완료 기준을 `답변 완료`로 할지 `종료`로 할지 +- [ ] R2 bucket, endpoint, region, presigned URL 만료시간, 보존·삭제 정책 +- [ ] 기존 LOCAL 첨부파일을 모두 R2로 이관할 시점 + +## 8. 6·7번 구현 메모 + +- Secretary API는 `STORAGE_PROVIDER=R2`일 때 R2 S3-compatible endpoint로 신규 피드백 첨부와 댓글 이미지 바이너리를 저장한다. 기존 `LOCAL` 첨부는 로컬 경로에서 계속 다운로드할 수 있다. +- 로컬 기본값은 `STORAGE_PROVIDER=LOCAL`이며, R2 실제 업로드를 확인하려면 로컬 `apps/secretary-api/.env`에 `R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`을 별도로 설정해야 한다. +- 운영 지표는 `/api/tickets/metrics?projectId={projectId}&channelId={channelId}`에서 프로젝트·채널별로 제공하며, 오늘 신규·처리 대기·완료 건의 평균 처리 시간을 계산한다. + +## 9. 참고한 기존 구현 위치 + +- 프로토타입 화면: `qna-platform-prototype-2.html` +- 티켓 모델: `apps/secretary-api/app/db/models.py` +- 티켓 스키마: `apps/secretary-api/app/schemas/ticket.py` +- 티켓/댓글/첨부파일 처리: `apps/secretary-api/app/services/ticket_service.py` +- 티켓 API: `apps/secretary-api/app/api/routes/tickets.py` +- 관리자 지원 콘솔 셸: `apps/web/src/features/support-portal/ui/support-operations-shell.ui.tsx` + diff --git a/docs/관리페이지 md 파일/ssot-feedback-rearchitecture-tasks.md b/docs/관리페이지 md 파일/ssot-feedback-rearchitecture-tasks.md new file mode 100644 index 0000000..485dcc8 --- /dev/null +++ b/docs/관리페이지 md 파일/ssot-feedback-rearchitecture-tasks.md @@ -0,0 +1,233 @@ +# 피드백 SSOT 구조 개편 Task + +> 추가 진행 메모 (2026-08-20): ABC 채널의 `feedback_status` 6단계 필드를 사용하도록 목록·상세·칸반 상태 변경을 연결했다. 댓글은 ABC `feedback_comments`로 저장하고 `is_internal`로 공개 댓글과 내부 메모를 분리했으며, 댓글 BFF는 세션 사용자·테넌트 기준 수정/삭제 권한을 검증한다. ABC 워크스페이스의 댓글 첨부파일은 Secretary로 중복 저장하지 않도록 명시적으로 차단했다. + +> 추가 진행 메모 (2026-08-20): ABC 댓글 CRUD와 댓글 첨부파일 메타데이터/업로드 API를 구현했다. 첨부파일은 채널의 R2 설정을 사용해 private object로 저장하고 5분 만료 presigned URL로 조회한다. 로컬 egbim 채널에 R2 설정을 저장하고 연결 검증을 완료했다. + +> 추가 진행 메모 (2026-08-20): 활성 Secretary 댓글 11건(공개 댓글 4건, 내부 메모 7건)과 댓글 첨부파일 4건을 ABC DB/R2로 이전했다. 이전 스크립트는 중복 실행 시 기존 데이터를 건너뛴다. + + +> 진행 메모 (2026-08-20): ABC에 저장된 관리자 필드(`IP`, `MAC_address`, `Category`)를 사용자 작성 폼의 `ip_address`, `mac_address`, `category`로 매핑했다. Category는 ABC의 선택 옵션을 그대로 표시하고, 생성·수정·상세 조회가 ABC API를 사용하도록 연결했다. Secretary fallback 제거와 인증 기반 requester 처리 등 전체 SSOT 전환은 계속 진행 중이다. +추가 진행 메모: ABC의 IP/MAC/Category 및 requester 메타데이터를 사용자 폼·단일 BFF에 연결했고, 인증 requester/tenant와 소유권 검증을 적용했다. 사용자 페이지의 테스트 상수와 support-stub 타입 의존성은 제거했다. 상태 변경은 Secretary가 아니라 ABC의 `feedback_status` 필드를 통해 처리한다. + +## 1. 목표 + +피드백 데이터의 원본을 ABC API/DB 한 곳으로 통일한다. + +관리자 콘솔과 사용자 피드백 페이지는 동일한 ABC API를 사용하고, 사용자 페이지는 데이터 저장소나 자체 fallback 데이터를 갖지 않는 API 클라이언트로 동작한다. + +```text +ABC API/DB + └─ 피드백 데이터의 유일한 원본(SSOT) + +관리자 콘솔 + └─ ABC API를 통한 조회·관리 화면 + +사용자 피드백 페이지 + └─ ABC API를 통한 조회·작성·수정·삭제 화면 + +Secretary API + └─ SSO·접근권한·운영 보조 기능 +``` + +관리자 콘솔 화면 자체가 원본은 아니며, 콘솔이 사용하는 ABC API/DB가 원본이다. + +## 2. 현재 문제 + +- 피드백이 Secretary DB와 ABC DB에 중복 저장됨 +- 사용자 페이지의 Next.js API route에 `support-stub` fallback이 존재함 +- 폼 템플릿 원본이 Secretary DB와 로컬 stub으로 분산됨 +- 사용자 작성 시 테스트용 requester/tenant 값이 사용됨 +- IP/MAC이 Secretary의 `extra_fields`에만 저장되고 ABC 피드백 원본에는 전달되지 않음 +- 사용자 페이지와 관리자 콘솔이 서로 다른 데이터 경로를 사용할 수 있음 +- Secretary 저장 성공과 ABC 저장 성공 사이의 데이터 불일치 가능성이 있음 + +## 3. SSOT 정책 + +### ABC API/DB가 보유하는 원본 데이터 + +- 피드백 ID +- 제목 및 내용 +- 작성자 및 작성자 연락처 +- 카테고리 +- 비밀글 여부 +- 첨부파일 및 첨부파일 메타데이터 +- IP 주소 및 MAC 주소 +- 피드백 중요도 +- 피드백 처리 상태 +- 생성일 및 수정일 +- 이슈 연결 정보 +- 관리자 댓글 및 내부 메모 + +피드백 상태와 이슈 상태는 서로 다른 필드와 생명주기로 관리한다. 이슈 상태를 피드백 상태의 원본으로 사용하지 않는다. + +### Secretary API가 보유할 수 있는 데이터 + +- SSO 인증 및 접근권한 +- 사용자·워크스페이스 접근 설정 +- 운영에 필요한 보조 매핑 + +Secretary DB에 피드백 원본을 복제 저장하지 않는다. 불가피한 매핑이 필요한 경우 `abc_feedback_id`를 외래 식별자로 사용하고, 피드백 제목·내용·상태를 별도로 저장하지 않는다. + +## 4. 단계별 Task + +### Phase 1. 데이터 및 API 설계 + +- [x] 현재 ABC 피드백 엔티티, 관리자 필드 설정, 채널 필드 구조 확인 +- [x] 피드백 원본 필드 목록 확정 +- [x] IP 주소 및 MAC 주소 저장 방식 확정 +- [x] 중요도 옵션과 색상 메타데이터 확정 +- [x] 피드백 6단계 상태 코드 및 표시명 확정 +- [x] 피드백 상태와 이슈 상태의 독립성 확인 +- [x] 댓글과 내부 메모의 저장 위치 및 공개 범위 확정 (ABC `feedback_comments`; `is_internal=false/true`) +- [x] 사용자용 API와 관리자용 API의 권한 범위 정의 +- [x] 피드백 CRUD API 계약서 및 응답 DTO 작성 +- [x] 페이지네이션, 정렬, 검색, 상태 필터 API 규격 확정 + - ABC 검색 API: `POST /api/v2/projects/:projectId/channels/:channelId/feedbacks/search` + - 요청: `page`, `limit`, `sort`, `queries`, `operator` + - 관리자 목록: ABC 네이티브 응답의 `meta` 기준 페이지 이동, 사용자 목록: BFF 응답을 10건 단위로 표시 + - 상태 필터: `feedback_status` 필드 query, 이슈 상태 필터와 분리 + +### Phase 2. ABC 백엔드에 원본 기능 구현 + +- [x] 피드백 엔티티에 IP 주소 필드 추가 +- [x] 피드백 엔티티에 MAC 주소 필드 추가 +- [x] 피드백 엔티티에 중요도 필드 추가 +- [x] 피드백 엔티티에 피드백 상태 필드 추가 +- [x] 댓글·내부 메모 저장 구조 추가 (ABC `feedback_comments` 테이블 및 API) +- [x] 댓글 첨부파일 메타데이터·R2 저장/삭제/서명 URL API 추가 (`feedback_comment_attachments`) +- [x] 이슈 연결 관계와 피드백 상태를 별도 필드로 유지 +- [x] 피드백 필드는 데이터베이스 migration 불필요 확인 (ABC JSON 데이터 + 채널 필드 설정 사용; 댓글은 별도 `feedback_comments` migration 적용) +- [x] 사용자 작성 API 구현 +- [x] 사용자 조회 API 구현 +- [x] 사용자 수정 API 구현 +- [x] 사용자 삭제 API 구현 +- [x] 관리자 상세 조회 API에 모든 피드백 필드 포함 +- [x] 관리자 수정 API에 IP/MAC·중요도·피드백 상태 포함 +- [x] API에서 requester/tenant를 인증 정보 또는 안전한 요청 정보로 처리 +- [x] 첨부파일 처리와 피드백 원본의 연결 보장 +- [ ] 중복 생성 방지를 위한 idempotency 또는 중복 요청 검증 추가 + +### Phase 3. 관리자 콘솔 개편 + +- [x] 관리자 피드백 목록이 ABC API만 조회하도록 통일 +- [x] 관리자 피드백 상세에 IP 주소와 MAC 주소 표시 +- [x] 관리자 수정 화면에서 IP 주소와 MAC 주소 표시·수정 +- [x] 중요도 옵션 및 배경색 표시 +- [x] 피드백 6단계 상태 및 배경색 표시 +- [x] 칸반 드래그 시 ABC API의 피드백 상태만 변경 +- [x] 이슈 상태는 이슈 API를 통해 별도로 변경 +- [x] 피드백과 이슈 상태가 서로 덮어쓰지 않는지 확인 +- [x] 댓글과 내부 메모의 저장 API 분리 +- [x] 내부 메모가 외부 댓글로 복제되지 않도록 보장 (`is_internal` 분리 및 ABC 워크스페이스 Secretary 첨부댓글 경로 차단) +- [x] 목록의 페이지네이션·정렬·필터를 ABC API 기준으로 통일 (관리자 native search; 사용자 BFF의 정렬·검색·10건 페이지) +- [x] fallback 또는 임시 데이터가 표시되지 않도록 처리 (ABC 워크스페이스는 오류 응답; 비-ABC 호환 경로만 유지) + +### Phase 4. 사용자 피드백 페이지 개편 + +- [x] 작성 페이지에서 관리자 필드 설정을 ABC API로 조회 +- [x] 페이지 내부의 `support-stub` 의존성 제거 +- [x] 페이지 내부의 테스트용 requester/tenant 상수 제거 +- [x] 사용자 인증 정보로 작성자 정보 처리 +- [x] 작성 요청을 ABC API로 직접 전달하거나 단일 BFF를 통해 전달 +- [x] 제목·내용·카테고리·비밀글·IP·MAC·첨부파일을 ABC API에 저장 +- [x] 사용자 본인이 작성한 피드백 목록을 ABC API로 조회 +- [x] 사용자 피드백 상세를 ABC API로 조회 +- [x] 사용자 수정·삭제 권한 검증 +- [x] 접근 불가·존재하지 않는 피드백에 대한 오류 처리 +- [x] API 오류 시 stub 데이터로 대체하지 않고 명확한 오류 표시 +- [x] 작성 성공 후 ABC 피드백 ID 기준으로 상세 또는 목록 이동 + +### Phase 5. Secretary API 정리 + +- [x] ABC 매핑 워크스페이스의 Secretary 피드백 생성·복제 경로 비활성화 (비-ABC 호환 경로는 별도 유지) +- [x] ABC 매핑 워크스페이스의 `SupportTicket` 피드백 원본 중복 저장 제거 (BFF는 `abc_feedback_id`로 합성) +- [x] 기존 매핑은 `abc_feedback_id` 중심으로 사용 +- [x] ABC 매핑 워크스페이스에서 Secretary는 SSO·접근권한·운영 보조 API로만 사용 +- [x] ABC 매핑 워크스페이스에서 Secretary 피드백 제목·내용·상태 수정 경로 차단 +- [x] ABC 매핑 워크스페이스의 기존 Secretary 기반 사용자 페이지 API route를 ABC BFF로 전환 +- [ ] `support-stub.ts` fallback 제거 +- [x] ABC 매핑 워크스페이스 장애 시 임의 데이터가 노출되지 않도록 오류 응답 처리 + +### Phase 6. 기존 데이터 이전 + +- [x] Secretary DB의 피드백 원본·댓글 데이터 목록 read-only inventory (로컬: 매핑 티켓 11건, 댓글/내부메모 14건) +- [x] ABC DB에 이미 존재하는 피드백과 매핑 확인 (로컬 ABC 피드백 27건, Secretary 매핑 11건) +- [x] 활성 매핑 댓글 11건 및 댓글 첨부파일 4건을 ABC DB/R2로 idempotent 이전 +- [x] 이전 실행 전 Secretary/ABC 백업 승인 및 백업 +- [ ] 누락된 ABC 피드백 생성 또는 이전 스크립트 작성 +- [ ] IP/MAC·중요도·상태·댓글·내부 메모 이전 범위 확정 (댓글/내부메모 14건 및 댓글 첨부파일 4건의 ABC 저장 방식·중복 처리 기준 결정 필요) +- [ ] 중복 피드백 병합 기준 확정 +- [x] `abc_feedback_id` 매핑 검증 +- [x] 이전 전 Secretary DB와 ABC DB 백업 +- [ ] staging에서 migration dry-run +- [ ] 이전 후 건수·ID·내용·상태 대조 +- [ ] 이전 완료 후 Secretary 중복 데이터 읽기 차단 + +### Phase 7. 테스트 + +#### API 테스트 + +- [ ] 관리자 피드백 목록·상세 조회 +- [ ] 사용자 피드백 작성 +- [ ] 사용자 피드백 조회 +- [ ] 사용자 피드백 수정 +- [ ] 사용자 피드백 삭제 +- [ ] IP/MAC 저장 및 조회 +- [ ] 중요도 저장 및 조회 +- [ ] 피드백 6단계 상태 변경 +- [ ] 이슈 상태와 피드백 상태의 독립 변경 +- [ ] 첨부파일 저장 및 조회 +- [ ] 댓글과 내부 메모 분리 +- [ ] 권한 없는 사용자의 수정·삭제 차단 +- [ ] 중복 요청 방지 + +#### 화면 테스트 + +- [ ] 관리자 콘솔과 사용자 페이지의 제목·내용·상태·중요도 일치 +- [ ] 작성 후 관리자 콘솔에 즉시 표시 +- [ ] 관리자 수정 후 사용자 페이지에 동일하게 표시 +- [ ] IP/MAC이 관리자 상세에서 표시 +- [ ] 사용자에게 내부 메모가 노출되지 않음 +- [ ] 10개 단위 페이지네이션 및 마지막 페이지 이동 +- [ ] 리스트/칸반 조회 결과 일치 +- [ ] 칸반 드래그 상태 변경 결과가 목록에도 반영 +- [ ] API 장애 시 stub 데이터가 노출되지 않음 + +### Phase 8. 배포 및 운영 검증 + +- [ ] staging DB 백업 +- [ ] migration 적용 +- [ ] staging API 배포 +- [ ] staging Web 배포 +- [ ] Secretary API의 변경된 권한·매핑 검증 +- [ ] 관리자 로그인 후 피드백 조회 +- [ ] 일반 사용자 로그인 후 피드백 작성 +- [ ] 작성 데이터가 ABC 관리자 콘솔에 표시되는지 확인 +- [ ] 관리자 수정 결과가 사용자 페이지에 반영되는지 확인 +- [ ] 브라우저 Console 및 Network 오류 확인 +- [ ] API 응답 401/403/404/500 확인 +- [ ] 데이터 건수 및 원본 ID 대조 + +## 5. 완료 기준 + +다음 조건을 모두 만족해야 SSOT 개편 완료로 판단한다. + +- 피드백 원본 데이터가 ABC API/DB 한 곳에만 존재한다. +- 관리자 콘솔과 사용자 페이지가 동일한 피드백 API를 조회한다. +- 사용자 페이지에 실제 데이터용 stub/fallback이 없다. +- Secretary DB에 피드백 제목·내용·상태를 중복 저장하지 않는다. +- IP/MAC·중요도·피드백 상태·이슈 연결 정보가 관리자 콘솔에서 조회된다. +- 피드백 상태와 이슈 상태가 독립적으로 처리된다. +- 사용자 작성·수정·삭제 결과가 관리자 콘솔에 동일하게 반영된다. +- 관리자 수정 결과가 사용자 페이지에 동일하게 반영된다. +- 내부 메모가 사용자에게 노출되지 않는다. +- 데이터 이전 후 중복·누락·불일치가 없다. + +## 6. 주의사항 + +- 운영 또는 staging DB에서 `down -v`를 실행하지 않는다. +- 기존 데이터 이전 전 DB 백업을 확보한다. +- ABC DB를 원본으로 전환하기 전 Secretary 기반 작성 API를 동시에 활성화하지 않는다. +- migration 적용 순서와 기존 데이터의 `abc_feedback_id` 매핑을 먼저 검증한다. +- ABC API 장애 시 임시 데이터를 노출하지 말고 오류 상태를 사용자에게 표시한다. diff --git a/docs/관리페이지 md 파일/staging-production-deployment-pipeline.md b/docs/관리페이지 md 파일/staging-production-deployment-pipeline.md new file mode 100644 index 0000000..034fe12 --- /dev/null +++ b/docs/관리페이지 md 파일/staging-production-deployment-pipeline.md @@ -0,0 +1,890 @@ +# 스테이징·운영 배포 파이프라인 및 DB 운영 설계 + +## 1. 목표 + +이 문서는 현재 데이터 아키텍처를 안전하게 배포하기 위한 기준 절차를 정의함. + +- 스테이징 서버와 운영 서버의 배포 흐름 +- 동일한 `userfeedback` 스키마를 공유하는 TypeORM·Alembic Migration 순서 +- UUIDv7 기반 ABC 데이터와 Secretary 지원 데이터의 호환성 검증 +- Docker 이미지 빌드와 서비스 기동 순서 +- 배포 전 DB 백업 및 네이버웍스 드라이브 보관 +- Redis·첨부파일 저장소·동적 workspace 매핑 검증 +- 환경변수 누락·오류·서비스 재시작 장애 대응 +- `eg-bim.com` 페이지 서버에서 중앙 관리 서버로 Q&A 데이터를 일회성 이관하는 방법 + +테이블의 상세 컬럼 설계는 별도 아키텍처 문서에서 관리하되, PK/FK 자료형과 +Migration 소유권처럼 배포 안전성에 직접 영향을 주는 데이터 규칙은 이 문서에 포함함. + +## 2. 환경 분리 + +| 구분 | 목적 | 데이터 | 배포 승인 | +| -------- | ----------------------------- | ------------------------------------- | ----------- | +| 개발 | 개발자 로컬 기능 확인 | 테스트 데이터 | 없음 | +| 스테이징 | 실제 배포 전 통합·회귀 테스트 | 운영과 분리된 복제 또는 테스트 데이터 | 담당자 확인 | +| 운영 | 실제 서비스 | 운영 데이터 | 수동 승인 | + +스테이징과 운영은 다음 항목을 반드시 분리함. + +- 서버 또는 Docker Compose 프로젝트명 +- 도메인과 포트 +- DB와 DB 계정 +- JWT secret +- API Key +- R2 버킷 또는 prefix +- SSO Client 설정 +- 네이버웍스 Bot과 알림 대상 +- 백업 파일 보관 경로 +- TypeORM `migrations` 및 Alembic `alembic_version` 실행 이력 + +운영용 비밀값을 스테이징에서 사용하지 않음. + +## 3. 현재 데이터·Docker 아키텍처 + +운영 Compose 기준 서비스는 다음과 같음. + +```text +web 관리자 웹 화면 +api ABC 관리 API, TypeORM Migration 소유자 +secretary-api 지원 API, Alembic Migration 소유자 +mysql ABC·Secretary가 공유하는 userfeedback 스키마 +redis 캐시·분산 잠금·멱등 처리 가속 계층 +``` + +현재 운영 Compose는 다음 파일을 사용함. + +```text +docker/docker-compose.prod.yml +``` + +### 3.1 데이터 소유권 + +| 영역 | 저장 위치 | 식별자·소유권 | Migration 이력 | +| --------------------- | ------------------------ | ------------------------------------------------------- | ------------------------- | +| ABC 핵심 테이블 | MySQL `userfeedback` | UUIDv7, MySQL `BINARY(16)` | TypeORM `migrations` | +| Secretary 지원 테이블 | 동일 MySQL 스키마 | Secretary 내부 PK는 자체 규격, ABC 참조값은 UUID 문자열 | Alembic `alembic_version` | +| 프로젝트·채널 연결 | Secretary mapping 테이블 | 이름으로 조회한 ABC UUID를 동적으로 저장 | Alembic | +| 멱등성 최종 상태 | MySQL | MySQL이 권위 저장소 | TypeORM/Alembic | +| 캐시·락 | Redis | 재구성 가능한 비권위 데이터 | 애플리케이션 설정 | +| 첨부파일 본문 | R2 또는 승인된 로컬 볼륨 | DB에는 메타데이터·객체 키 저장 | Storage 운영 절차 | + +ABC UUIDv7 전환 Migration은 Secretary가 소유한 숫자 PK까지 변환하면 안 됨. 반대로 +Alembic은 ABC 핵심 테이블의 PK/FK를 변경하지 않음. 공유 스키마에서 두 Migration +엔진을 동시에 실행하지 않으며, 배포 파이프라인이 실행 순서를 직렬화함. + +MySQL 관리 도구에서 `BINARY(16)` UUID가 깨진 문자열처럼 보이는 것은 정상임. 운영 +검증은 원시 셀 표시가 아니라 `BIN_TO_UUID(id)` 또는 애플리케이션 API의 UUID +문자열을 기준으로 수행함. + +### 3.2 네트워크 원칙 + +- API·Secretary·MySQL·Redis 간 통신은 내부 `backend` 네트워크를 사용함. +- Redis는 호스트에 직접 게시하지 않음. +- MySQL 관리 포트는 `database_access` 네트워크를 통해 지정된 내부 서버 IP에만 게시함. +- `MYSQL_BIND_ADDRESS=0.0.0.0` 또는 `::`는 허용하지 않음. +- 외부 브라우저에는 API Key와 내부 서비스 주소를 노출하지 않음. + +### 3.3 현재 staging workflow와 목표 상태 + +현재 `.gitea/workflows/deploy-staging.yml`은 소스를 SSH로 전송하고 배포 서버에서 +이미지를 빌드한 뒤 컨테이너를 재생성함. API는 시작 시 TypeORM Migration을 실행할 +수 있고, Secretary 이미지는 시작 명령에서 `alembic upgrade head`를 실행함. + +이 방식은 초기 스테이징에는 사용할 수 있지만 다음 한계가 있음. + +- 동일 DB에서 TypeORM과 Alembic이 기동 과정에 동시에 실행될 수 있음. +- Migration 전에 자동 백업과 복구 가능성 검증이 없음. +- 서버에서 다시 빌드하므로 CI에서 검증한 이미지와 실제 배포 이미지의 digest가 고정되지 않음. +- 소스 압축을 기존 디렉터리에 덮어써 삭제된 파일이 서버에 남을 수 있음. +- Web health 중심 검증만으로 UUID 스키마, Redis, SSO, 첨부파일 이상을 발견하기 어려움. + +목표 상태는 CI에서 한 번 생성한 commit SHA 이미지와 명시적 Migration Job을 +사용하는 것임. + +```text +코드 Push 또는 Tag + │ + ▼ +CI 테스트·Migration 정합성 검사 + │ + ▼ +동일 commit SHA의 이미지 3종 Build·Registry Push + │ + ▼ +배포 잠금 → 백업 → 순차 Migration → 앱 기동 → Smoke Test +``` + +배포 시 `latest` 대신 Git commit SHA 또는 release tag를 사용해야 이전 버전으로 되돌릴 수 있음. + +예시: + +```text +line/abc-user-feedback-api:20260915-abc1234 +line/abc-user-feedback-web:20260915-abc1234 +line/abc-user-feedback-secretary-api:20260915-abc1234 +``` + +## 4. 목표 배포 파이프라인 + +```text +[Pull Request] + │ + ▼ +[Lint·Typecheck·Unit/Integration Test·Migration 검사] + │ + ▼ +[Commit SHA 이미지 Build·Registry Push] + │ + ▼ +[배포 동시 실행 잠금] + │ + ▼ +[환경변수·Compose·DB 연결 Preflight] + │ + ▼ +[MySQL 전체 백업 + checksum] + │ + ▼ +[TypeORM Migration → ABC 스키마 검증] + │ + ▼ +[Alembic Migration → 지원 스키마 검증] + │ + ▼ +[API → Secretary API → Web 기동] + │ + ▼ +[스테이징 Smoke Test] + │ 성공 + ▼ +[운영 배포 승인] + │ + ▼ +[동일 SHA 이미지로 운영 승인 배포] + │ + ├─ 성공: 배포 완료 + └─ 실패: 서비스 롤백 또는 복구 절차 +``` + +### 4.1 CI 검증 단계 + +- Node.js 24.14.1과 고정된 pnpm 버전을 사용함. +- API lint·typecheck·unit/integration test·build를 실행함. +- Web lint·typecheck·unit test·production build를 실행함. +- Secretary API pytest·문법 검사·패키지 build를 실행함. +- `alembic heads`가 하나인지 확인함. +- TypeORM migration timestamp/name 중복과 미적용 migration을 확인함. +- `docker compose config`로 Production Compose를 검증함. +- Secret 또는 DB dump가 Git 변경사항에 포함되지 않았는지 확인함. + +CI 검증이 실패한 commit은 staging 서버로 전송하지 않음. + +### 4.2 이미지 생성 단계 + +- API, Web, Secretary API 이미지를 같은 commit 기준으로 생성함 +- 이미지에 commit SHA와 배포 버전을 함께 태깅함 +- Registry에 Push함 +- 빌드된 이미지 digest를 배포 기록에 남김 +- staging과 production은 이미지를 다시 빌드하지 않고 동일 digest를 Pull함 + +`latest`는 사람이 확인하는 보조 태그로만 사용하고 실제 배포 기준으로 사용하지 않음. + +### 4.3 배포 잠금과 Preflight + +- 환경별 동시 배포를 하나로 제한함. +- 필수 Variable·Secret의 존재 여부와 형식을 검사함. +- `MYSQL_BIND_ADDRESS`가 wildcard 주소가 아닌지 검사함. +- MySQL·Redis 연결과 R2 bucket 접근 권한을 검사함. +- 현재 TypeORM/Alembic revision과 배포 대상 revision을 기록함. +- UUID 전환 같은 비호환 Migration이면 maintenance mode 또는 쓰기 중지를 먼저 적용함. + +### 4.4 DB 백업 + +- ABC와 Secretary가 같은 스키마를 사용하므로 `userfeedback` 전체를 한 번에 백업함. +- TypeORM 테이블만 또는 `support_*` 테이블만 선택 백업하지 않음. +- 백업 파일과 checksum이 생성되지 않으면 Migration을 시작하지 않음. +- 첨부파일이 로컬 저장소라면 같은 배포 시점의 업로드 디렉터리도 함께 백업함. +- R2 사용 시 DB backup 시각과 bucket/versioning 상태를 배포 기록에 남김. + +### 4.5 Migration 직렬 실행 + +정상 배포에서는 앱 기동에 Migration 실행 책임을 두지 않음. + +```text +1. mysql·redis health 확인 +2. TypeORM 전용 one-shot Job 실행 +3. TypeORM migrations 이력 및 ABC UUID 스키마 검증 +4. Alembic 전용 one-shot Job에서 upgrade head 실행 +5. alembic_version 및 workspace mapping 스키마 검증 +6. 두 Job이 모두 성공한 경우에만 애플리케이션 기동 +``` + +목표 설정에서는 API 컨테이너의 `AUTO_MIGRATION=false`를 기본값으로 사용하고, +Secretary API의 `alembic upgrade head`도 서비스 CMD에서 분리해 별도 Job으로 실행함. + +Migration 순서는 기본적으로 TypeORM → Alembic임. 두 migration이 서로의 테이블이나 +참조 컬럼을 변경하는 release는 PR에 의존 순서와 호환성 검증 SQL을 함께 포함함. + +### 4.6 애플리케이션 기동 + +- MySQL·Redis가 healthy인지 확인함. +- API를 먼저 기동하고 `/api/health`를 확인함. +- Secretary API를 기동하고 `/api/health` 및 ABC 내부 API 연결을 확인함. +- Web을 마지막에 기동하고 외부 health를 확인함. +- 새 컨테이너 검증이 끝나기 전에 이전 release artifact를 삭제하지 않음. + +### 4.7 staging 승격과 운영 배포 + +- staging에서 검증한 동일 image digest만 production으로 승격함. +- production은 Gitea Environment 승인 후 실행함. +- 운영 DB 백업과 checksum 확인 후에만 Migration을 실행함. +- 운영 Smoke Test가 완료될 때까지 이전 image digest와 DB backup을 보존함. + +### 4.8 현재 workflow에서 목표 상태로 전환할 작업 + +- [ ] staging deploy job 앞에 CI 검증 job 추가 +- [ ] 환경별 concurrency lock 추가 +- [ ] 서버 build 대신 commit SHA 이미지 Registry Push/Pull 방식으로 전환 +- [ ] 소스 덮어쓰기 배포 제거 또는 release 디렉터리 교체 방식 적용 +- [ ] 배포 직전 MySQL 전체 backup Job 추가 +- [ ] TypeORM과 Alembic을 별도 one-shot Migration Job으로 분리 +- [ ] 애플리케이션 기동 시 자동 Migration 비활성화 +- [ ] Migration 전후 revision·schema 검증 단계 추가 +- [ ] API·Secretary·Redis·SSO·첨부파일 Smoke Test 자동화 +- [ ] 배포 결과에 commit SHA, image digest, migration revision, backup checksum 기록 + +## 5. 배포 전 DB 백업 + +### 백업 원칙 + +- staging의 비호환 Migration과 모든 운영 배포 전 backup을 필수로 실행함 +- ABC와 Secretary가 공유하는 `userfeedback` 스키마 전체를 동일 시점에 백업함 +- 백업 성공과 네이버웍스 드라이브 업로드 성공을 모두 확인한 뒤 배포함 +- 백업 파일은 압축하고 암호화함 +- 파일명에 환경, 생성 시각, 배포 버전, commit SHA를 포함함 +- 백업 파일의 SHA-256 checksum을 함께 보관함 +- TypeORM `migrations`와 Alembic `alembic_version` 테이블을 반드시 포함함 +- UUID `BINARY(16)` 값 보존을 위해 `--hex-blob` 옵션을 사용함 + +### 백업 파일 예시 + +```text +prod-userfeedback- +2026-09-15T103000Z- +release-2026.09.15- +abc1234.sql.gz.age +``` + +### MySQL 백업 예시 + +실제 계정과 비밀번호는 명령행에 직접 입력하지 않고 Secret 또는 보호된 환경변수로 주입함. + +```bash +mysqldump \ + --single-transaction \ + --routines \ + --triggers \ + --events \ + --hex-blob \ + --set-gtid-purged=OFF \ + -h "$MYSQL_HOST" \ + -u "$MYSQL_USER" \ + "$MYSQL_DATABASE" \ + | gzip \ + | age -r "$BACKUP_AGE_RECIPIENT" \ + > "$BACKUP_FILE" +``` + +백업 후 다음을 확인함. + +```bash +test -s "$BACKUP_FILE" +sha256sum "$BACKUP_FILE" > "$BACKUP_FILE.sha256" +``` + +압축·암호화 없이 네이버웍스 드라이브에 업로드하지 않음. 백업 파일에는 개인정보가 포함될 수 있으므로 접근 권한을 제한함. + +복원 리허설에서는 빈 임시 MySQL에 backup을 복원한 뒤 다음을 확인함. + +- TypeORM `migrations` 최종 행과 Alembic `alembic_version` 값 +- ABC UUID PK/FK 컬럼의 `BINARY(16)` 자료형 +- Secretary의 ABC 참조 컬럼이 UUID 문자열을 수용하는지 여부 +- 프로젝트·채널·피드백·댓글·첨부 메타데이터 건수 +- FK 위반과 고아 mapping 존재 여부 + +Redis는 권위 저장소가 아니므로 일반 DB backup 대상에서 제외함. Redis 장애나 유실 시 +MySQL 기준으로 캐시와 잠금을 재구성할 수 있어야 함. 단, 배포 직전에 진행 중인 비동기 +작업이 있다면 drain 또는 완료를 확인함. + +R2 객체 본문은 MySQL dump에 포함되지 않음. bucket versioning 또는 별도 객체 backup +정책을 사용하고 DB의 attachment metadata와 객체 존재 여부를 표본 검증함. + +### 네이버웍스 드라이브 보관 + +네이버웍스 드라이브 업로드는 다음 중 하나로 구현함. + +| 방식 | 사용 조건 | +| ---------------------------------- | ---------------------------------------------------------- | +| 네이버웍스 Drive API 연동 스크립트 | API 사용 권한과 OAuth 또는 서비스 계정이 준비된 경우 | +| 백업 전용 동기화 Agent | 운영 서버에서 승인된 동기화 프로그램을 사용할 수 있는 경우 | +| 별도 백업 작업 서버 | 운영 서버에 외부 업로드 권한을 주지 않는 경우 | + +업로드 방식과 관계없이 다음 결과를 배포 로그에 남김. + +- 원본 백업 파일명 +- 파일 크기 +- SHA-256 checksum +- 네이버웍스 드라이브 파일 ID 또는 경로 +- 업로드 시작·완료 시각 +- 업로드한 환경과 배포 버전 + +네이버웍스 드라이브 API의 인증 방식과 파일 업로드 API가 계정 계약 범위에서 활성화되어 있는지 먼저 확인함. Bot API의 메시지 전송 권한만으로는 드라이브 파일 업로드가 되지 않을 수 있음. + +### 보관 정책 + +| 구분 | 보관 기간 | +| -------------- | ----------------------------------: | +| 일일 백업 | 최근 30일 | +| 주간 백업 | 최근 12주 | +| 월간 백업 | 최근 12개월 | +| 배포 직전 백업 | 다음 주요 배포 전까지 최소 1개 유지 | + +삭제 작업은 보관 기간과 파일 checksum을 확인한 뒤 수행함. + +## 6. 환경변수와 Secret 관리 + +### 관리 원칙 + +- 값은 Git 저장소에 커밋하지 않음 +- 스테이징과 운영 Secret을 분리함 +- 환경변수 이름은 두 환경에서 동일하게 유지함 +- Secret 값은 CI 로그, Compose 출력, 애플리케이션 로그에 표시하지 않음 +- 변경자는 변경 시각과 변경 사유를 기록함 +- 변경 후 관련 서비스만 재생성함 + +### 주요 환경변수 분류 + +| 영역 | 주요 값 | 적용 서비스 | +| ------------ | ------------------------------------------------------------------------------------------------------- | ------------------------- | +| 기본 URL | `BASE_URL`, `ADMIN_WEB_URL`, `NEXT_PUBLIC_API_BASE_URL` | API, Web | +| 인증 | `JWT_SECRET`, `SSO_ISSUER`, `SSO_CLIENT_ID`, `SSO_CLIENT_SECRET`, `ALLOW_OAUTH_EMAIL_LINKING` | API, Secretary API | +| 내부 API | `MASTER_API_KEY`, `SECRETARY_ABC_API_KEY` | Web, API, Secretary API | +| DB 계정 | `ABC_DB_USER`, `ABC_DB_PASSWORD`, `ABC_DB_ROOT_PASSWORD` | MySQL, workflow | +| DB 연결 | workflow가 생성하는 `MYSQL_PRIMARY_URL`, `SECRETARY_DATABASE_URL` | API, Secretary API | +| DB 관리 포트 | `MYSQL_BIND_ADDRESS`, `MYSQL_PORT` | MySQL | +| Redis | `REDIS_PASSWORD`, workflow가 생성하는 `REDIS_URL` | API, Secretary API, Redis | +| 첨부파일 | `STORAGE_PROVIDER`, `R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_REGION` | Secretary API | +| 외부 연동 | `GITEA_*`, `JIRA_*`, `NAVER_WORKS_*` | API | +| 메일 | `SMTP_*` | API | + +### Secret 등록 후 필수 절차 + +```text +Secret 등록 + │ + ▼ +배포 Workflow 실행 + │ + ▼ +운영 서버에서 새 이미지·환경변수 반영 + │ + ▼ +컨테이너 재생성 + │ + ▼ +Health check와 기능 확인 +``` + +Secret 저장만으로 실행 중인 컨테이너의 환경변수가 바뀌지 않음. 반드시 새 배포 또는 `docker compose up -d --force-recreate`가 필요함. + +DB 초기화는 정상 배포 절차가 아님. DB를 의도적으로 재생성한 경우에만 다음 bootstrap +절차를 별도로 수행함. + +1. TypeORM과 Alembic schema를 순서대로 생성함. +2. 최초 tenant·관리자·프로젝트·채널을 생성함. +3. ABC API Key를 새로 발급함. +4. Gitea `SECRETARY_ABC_API_KEY` 값을 교체함. +5. SSO bootstrap 응답과 동일 이메일 계정 연결을 검증함. +6. 동적 workspace mapping이 새 프로젝트·채널 UUID를 조회하는지 확인함. + +기존 DB volume이 있는 상태에서 `ABC_DB_PASSWORD`만 변경하면 MySQL 내부 계정의 실제 +비밀번호와 애플리케이션 접속 비밀번호가 달라질 수 있음. Secret 교체 전 `ALTER USER`와 +애플리케이션 재배포를 하나의 승인된 작업으로 수행함. + +### 배포 전 변수 점검 + +값 자체를 출력하지 말고 존재 여부와 형식만 확인함. + +```bash +required_vars=( + JWT_SECRET + BASE_URL + ADMIN_WEB_URL + NEXT_PUBLIC_API_BASE_URL + SSO_ISSUER + SSO_CLIENT_ID + SSO_CLIENT_SECRET + ALLOW_OAUTH_EMAIL_LINKING + ABC_DB_USER + ABC_DB_PASSWORD + ABC_DB_ROOT_PASSWORD + REDIS_PASSWORD + MYSQL_BIND_ADDRESS + MYSQL_PORT + MASTER_API_KEY + SECRETARY_ABC_API_KEY + ADMIN_CANDIDATE_TENANT_ID + INITIAL_SUPER_ADMIN_PHONE_NUMBER + DEFAULT_SUPPORT_WORKSPACE_CODE + STORAGE_PROVIDER + R2_ENDPOINT + R2_ACCESS_KEY_ID + R2_SECRET_ACCESS_KEY + R2_BUCKET + R2_REGION +) + +for name in "${required_vars[@]}"; do + if [ -z "${!name:-}" ]; then + echo "missing required secret: $name" >&2 + exit 1 + fi +done +``` + +운영 Compose에 `STORAGE_PROVIDER=R2`를 사용하는 경우 R2 관련 다섯 값이 모두 존재해야 함. `JWT_SECRET`은 토큰을 검증하는 모든 서비스에서 동일해야 함. + +현재 staging workflow는 `STORAGE_PROVIDER` 값과 관계없이 R2 관련 값을 모두 필수 +Secret으로 검사함. 추후 `LOCAL` 저장소를 staging에서 허용하려면 검증 로직을 provider에 +따라 조건부로 변경해야 함. + +`MYSQL_BIND_ADDRESS`는 wildcard 주소를 금지하고 승인된 내부 인터페이스만 허용함. +프로젝트·채널 UUID는 환경변수로 고정하지 않고 런타임 동적 mapping을 사용함. + +## 7. 환경변수 오류 대응 + +### 증상별 원인 + +| 증상 | 가능 원인 | 우선 확인 | +| ------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------- | +| Compose 실행 시 `variable is not set` | 서버 환경에 값이 없음 | Secret 주입 경로와 변수명 | +| 컨테이너가 반복 재시작 | 잘못된 값, Migration 실패, 앱 시작 예외 | `docker compose logs`, 컨테이너 Exit Code | +| API와 Secretary가 동시에 DB 오류 | 두 Migration 엔진의 동시 실행 또는 schema 불일치 | `migrations`, `alembic_version`, MySQL metadata lock | +| 로그인 실패 | `JWT_SECRET` 불일치, SSO 설정 오류 | Web·API·Secretary API의 Secret 일치 여부 | +| SSO 버튼이 없음 | 초기 tenant에 OAuth 설정이 없고 SSO 환경변수 전달 실패 | `/api/admin/tenants`, API 컨테이너 SSO 설정 | +| SSO `User Already Exists` | 동일 이메일 자동 연결 비활성화 | `ALLOW_OAUTH_EMAIL_LINKING` | +| 첨부파일만 500 | R2 변수 누락·권한·endpoint 오류 | `STORAGE_PROVIDER`, R2 bucket, API key 권한 | +| Secretary에서 ABC 호출 401 | DB 재생성 후 과거 API Key 사용 | `SECRETARY_ABC_API_KEY` 재발급·재배포 | +| Redis 오류 | 비밀번호 불일치 또는 내부 network 이상 | Redis `PING`, `REDIS_URL` | +| HeidiSQL `10061` | MySQL host port가 실제 게시되지 않음 | `docker port`, `MYSQL_BIND_ADDRESS`, `database_access` network | +| HeidiSQL `1045` | MySQL 계정·비밀번호·host grant 불일치 | `ABC_DB_USER`, `ABC_DB_PASSWORD`, MySQL grants | +| API health는 정상이나 기능 실패 | 외부 API Key, URL, CORS 오류 | API 요청 로그와 외부 연동 설정 | +| 화면이 빈 화면 | `NEXT_PUBLIC_API_BASE_URL` 빌드값 오류 | Web 이미지 빌드 시점의 공개 URL | + +### 대응 순서 + +현재 staging workflow가 전달하는 환경변수는 원격 배포 프로세스에만 존재하고 서버의 +영구 `.env` 파일로 저장되지 않음. 따라서 SSH 접속 후 필수 값을 다시 주입하지 않은 +상태에서 `docker compose -f ... ps`만 실행하면, 컨테이너가 정상이어도 Compose 보간 +단계에서 `MASTER_API_KEY is required` 같은 오류가 발생할 수 있음. + +단순 상태·로그 확인에는 환경변수 보간이 필요 없는 Docker 명령을 우선 사용함. + +```bash +docker ps \ + --filter label=com.docker.compose.project=abc-user-feedback-deploy \ + --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + +docker logs --tail=200 abc-user-feedback-deploy-secretary-api-1 +docker logs --tail=200 abc-user-feedback-deploy-api-1 +docker logs --tail=200 abc-user-feedback-deploy-web-1 +``` + +서비스 재생성이나 `docker compose config`가 필요하면 Gitea workflow를 다시 실행하는 +것을 기본으로 함. 수동 작업이 승인된 환경에서만 권한이 제한된 환경 파일을 준비하여 +`docker compose --env-file <승인된 파일> -f docker/docker-compose.prod.yml ...` 형식으로 +실행하고, Secret을 셸 기록이나 저장소 파일에 남기지 않음. + +그 다음 순서로 확인함. + +1. 필수 Secret 이름이 Compose의 `${...}` 이름과 동일한지 확인함 +2. Secret 등록 후 Workflow를 다시 실행했는지 확인함 +3. 현재 방식은 새 이미지가 빌드되었는지, 목표 방식은 기록된 digest가 Pull되었는지 확인함 +4. 컨테이너가 새 환경변수로 재생성되었는지 확인함 +5. TypeORM과 Alembic revision이 배포 대상과 일치하는지 확인함 +6. 앱 내부 로그에 Secret 값이 아닌 오류 유형만 확인함 +7. 수정 후 해당 서비스만 재배포함 + +민감한 값 확인이 필요하면 값 전체를 출력하지 않고 길이, 해시, 존재 여부만 확인함. + +## 8. 배포 후 검증 + +### 컨테이너 상태 + +```bash +docker ps \ + --filter label=com.docker.compose.project=abc-user-feedback-deploy \ + --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' +``` + +모든 서비스가 `Up` 상태인지 확인함. + +MySQL host port는 다음 명령으로 실제 게시 여부를 확인함. `HostConfig`의 요청값만 +확인하지 말고 `docker port` 결과를 기준으로 판단함. + +```bash +docker port abc-user-feedback-deploy-mysql-1 3306/tcp +``` + +정상 staging 출력은 `10.13.10.4:13306`임. + +Redis는 외부에 게시하지 않고 컨테이너 내부 CLI로 확인함. + +```bash +docker exec abc-user-feedback-deploy-redis-1 \ + sh -lc 'REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping' +``` + +정상 출력은 `PONG`임. + +### Migration 상태 + +배포 로그와 DB에서 다음 두 이력이 모두 목표 revision인지 확인함. + +```text +TypeORM : userfeedback.migrations +Alembic : userfeedback.alembic_version +``` + +추가로 다음 불변 조건을 자동 검증함. + +- ABC PK와 직접 FK가 `BINARY(16)`인지 확인 +- API가 반환하는 프로젝트·채널·피드백 ID가 UUIDv7인지 확인 +- Secretary mapping의 ABC 식별자가 36자 UUID 문자열인지 확인 +- 숫자형 과거 mapping이 활성 상태로 남아 있지 않은지 확인 +- FK 위반과 중복 unique key가 없는지 확인 + +### API Health + +```bash +curl --fail --silent --show-error \ + https://feedback.hmac.kr/api/health +``` + +운영 Compose에서 Secretary API의 8010 포트는 호스트에 게시하지 않고 `expose`만 함. +따라서 호스트의 `127.0.0.1:8010`이 아니라 컨테이너 내부에서 확인함. + +```bash +docker exec abc-user-feedback-deploy-secretary-api-1 \ + python3 -c 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:8010/api/health", timeout=5)' +``` + +### 기능 Smoke Test + +- `/api/admin/tenants`의 `useOAuth=true`와 OAuth secret 마스킹 확인 +- 기존 관리자 이메일의 최초 SSO 연결 및 관리자 로그인 +- 프로젝트와 채널 목록 조회 +- 프로젝트명·채널명 기반 URL 접근과 UUID 내부 조회 확인 +- 동적 workspace mapping 조회 및 새 UUID 반영 확인 +- 피드백 목록 조회 +- 피드백 상세 조회 +- 상태 변경과 담당자 변경 +- 중요도 변경 +- 내부 메모 저장 +- 첨부파일이 포함된 피드백 등록 +- 첨부파일 다운로드 +- 동일 `Idempotency-Key` 재요청 시 중복 생성 방지 확인 +- 네이버웍스 알림이 필요한 경우 알림 수신 + +첨부파일은 API health만으로 검증되지 않으므로 별도 업로드 테스트가 필요함. + +## 9. 실패 시 롤백 + +### 애플리케이션 이미지 오류 + +DB 변경이 없거나 새 스키마가 이전 앱과 호환된다면 이전 release로 되돌림. + +현재 staging workflow는 서버에서 소스를 다시 빌드하므로 `RELEASE_TAG`만 바꿔서 +롤백할 수 없음. 이전의 검증된 commit을 Gitea workflow로 다시 배포해야 함. 이때도 +현재 DB schema가 이전 commit과 호환되는지 먼저 확인함. + +목표 Registry 방식으로 전환한 뒤에는 이전에 기록한 digest를 지정하여 롤백함. + +```bash +API_IMAGE=registry.example/api@sha256: +WEB_IMAGE=registry.example/web@sha256: +SECRETARY_IMAGE=registry.example/secretary-api@sha256: +``` + +실제 재기동은 승인된 배포 workflow가 위 digest를 받아 수행하며, 서버에서 임의의 +`latest` 이미지를 Pull하지 않음. + +### Migration 오류 + +- Migration 로그와 실패한 SQL을 확인함 +- TypeORM과 Alembic 중 어느 엔진이 실패했는지 구분함 +- 후속 Migration을 실행하지 않고 앱 기동을 중단함 +- 앱 컨테이너만 이전 이미지로 내리는 방식은 스키마 호환성을 먼저 확인함 +- 이미 일부 스키마가 변경됐다면 무조건 DB를 즉시 복원하지 않음 +- 호환 가능한 수정 Migration을 우선 검토함 +- 데이터 손상 또는 되돌릴 수 없는 변경이면 배포 전 백업으로 복원함 + +UUID PK 변환처럼 테이블 재작성과 PK/FK 교체를 포함하는 Migration은 일반적인 down +Migration 대상으로 보지 않음. 실패 시 쓰기를 중지하고 backup 복원 또는 검증된 roll-forward +Migration 중 하나를 선택함. 동일 DB를 공유하므로 ABC 테이블만 복원하거나 Secretary +테이블만 복원하지 않음. + +### 복원 원칙 + +```text +장애 확인 + │ + ├─ 앱 버그만 있음 → 이전 이미지로 롤백 + ├─ 환경변수 오류 → Secret 수정 후 컨테이너 재생성 + ├─ Migration 실패 → 호환성 확인 후 수정 또는 복원 + └─ 데이터 손상 → 백업 checksum 확인 후 DB 복원 +``` + +복원 후에는 두 Migration 이력, 목록 수, 최근 등록 글, workspace mapping, 첨부파일 +연결, 로그인 상태를 확인함. R2 객체를 별도로 복원한 경우 DB backup 시점과 객체 +version 시점이 일치하는지도 확인함. + +### 배포 기록 + +성공·실패 여부와 관계없이 배포마다 다음 정보를 남김. + +- 환경, 실행자, 승인자, 시작·종료 시각 +- Git commit SHA와 세 이미지 digest +- 배포 전후 TypeORM migration 및 Alembic revision +- DB backup 파일명, 크기, SHA-256 checksum, 보관 위치 +- 적용한 Variable·Secret의 이름과 버전 또는 변경 시각(값은 기록하지 않음) +- Smoke Test 결과와 실패한 단계 +- 롤백한 경우 이전 digest 또는 복원한 backup 식별자 + +## 10. `eg-bim.com` Q&A 데이터 일회성 이관 + +### 기본 원칙 + +`eg-bim.com`의 DB에 중앙 관리 서버가 직접 접속하지 않음. 기존 페이지 서버에 **읽기 전용 이관 API**를 만들고, 중앙 서버의 일회성 작업이 HTTPS API로 데이터를 가져오는 방식으로 구성함. + +```text +[eg-bim.com 페이지 서버] + │ 읽기 전용 HTTPS API + ▼ +[일회성 Import Worker] + │ 중앙 관리 API + ▼ +[중앙 API·MySQL·첨부파일 저장소] +``` + +### 10.1 원본 서버에 제공할 API + +페이지 서버에 다음 형태의 인증된 조회 API를 추가함. + +```http +GET /api/migration/qna?cursor=...&limit=100 +Authorization: Bearer +``` + +필요한 응답 예시: + +```json +{ + "items": [ + { + "source_id": 619, + "project_code": "EG-BIM", + "channel_code": "QNA", + "category": "오류문의", + "title": "도면 인쇄 시 화면이 멈추는 현상", + "content": "문의 내용", + "author": { + "name": "홍길동", + "email": "user@example.com", + "phone": "01000000000", + "department": "안전진단부" + }, + "status": "답변대기", + "created_at": "2026-09-14T14:29:45+09:00", + "updated_at": "2026-09-14T14:29:45+09:00", + "source_url": "https://eg-bim.com/egbim/bbs/descope_qa_detail.php?id=619", + "attachments": [] + } + ], + "next_cursor": "...", + "has_more": true +} +``` + +필드명과 상태값은 중앙 API가 사용하는 규격에 맞춰 사전에 확정함. + +### 10.2 원본 API 보안 + +- HTTPS만 사용함 +- 일회성 토큰 또는 이관 전용 API Key를 사용함 +- 토큰에는 만료 시각을 설정함 +- 중앙 Import Worker의 IP만 허용함 +- 페이지 조회 API가 아닌 별도 이관 API로 분리함 +- 이관이 끝나면 토큰을 폐기하고 API를 비활성화함 +- 요청·응답 로그에 비밀번호, 토큰, 개인정보 원문을 남기지 않음 +- 한 번에 가져올 수 있는 페이지 크기를 제한함 + +### 10.3 첨부파일 이관 + +첨부파일은 원본 페이지의 임시 URL을 중앙에 저장하는 방식으로 처리하지 않음. + +```text +원본 첨부파일 다운로드 + │ + ▼ +Import Worker가 파일 checksum 확인 + │ + ▼ +중앙 첨부파일 API 또는 R2 업로드 + │ + ▼ +중앙 글과 첨부파일 연결 +``` + +첨부파일 응답에는 다음 정보가 필요함. + +```json +{ + "source_file_id": "file-619-1", + "file_name": "screenshot.png", + "content_type": "image/png", + "size": 121856, + "download_url": "https://eg-bim.com/...", + "sha256": "..." +} +``` + +파일 다운로드가 가능한 동안에만 이관을 실행하고, 업로드 후 크기와 checksum을 비교함. + +### 10.4 중앙 Import Worker + +Import Worker는 운영 API 서버와 분리된 일회성 Docker 작업으로 실행함. + +```text +1. 원본 API 연결 확인 +2. 전체 건수 또는 첫 페이지 조회 +3. Dry Run으로 필드·프로젝트·채널 매핑 확인 +4. 스테이징 DB에 전체 이관 +5. 건수·상태·첨부파일 검증 +6. 운영 배포 전 DB 백업 +7. 운영 DB에 이관 +8. 이관 결과 리포트 저장 +9. 원본 이관 토큰 폐기 +``` + +실행 예시: + +```bash +docker compose -f docker/docker-compose.prod.yml run --rm \ + -e SOURCE_API_BASE_URL \ + -e SOURCE_API_TOKEN \ + -e CENTRAL_API_BASE_URL \ + -e CENTRAL_API_KEY \ + import-worker \ + python -m app.import_egbim_qna --dry-run +``` + +실제 운영 Compose에는 `import-worker`를 상시 서비스로 띄우지 않고, 필요할 때만 실행하는 일회성 Job으로 추가함. + +원본의 숫자 `source_id`는 중앙 feedback PK로 사용하지 않음. 중앙 feedback은 정상 +등록 흐름에서 UUIDv7을 새로 발급하고, `source_system + source_id`는 이관 추적과 중복 +방지용 별도 unique key로만 저장함. `project_code`와 `channel_code`는 실행 시 동적 +workspace mapping으로 현재 ABC UUID를 조회하며 UUID를 환경변수에 고정하지 않음. + +### 10.5 중복 이관 방지 + +이관 작업은 중간에 중단되거나 재실행될 수 있으므로 다음 값으로 이미 처리한 원본 글을 확인함. + +```text +source_system = eg-bim +source_id = 원본 Q&A 번호 +``` + +중앙 API는 동일한 원본 식별자로 다시 요청해도 새 글을 만들지 않고 기존 결과를 반환하도록 구성함. + +Import Worker에는 다음 기능을 포함함. + +- `--dry-run`: DB에 저장하지 않고 매핑만 검증함 +- `--limit`: 일부 건만 시험함 +- `--from-id`, `--to-id`: 범위를 지정함 +- `--resume`: 마지막 성공 cursor부터 재개함 +- 성공·실패·건너뜀 건수 기록 +- 실패한 원본 ID 목록 파일 저장 + +### 10.6 이관 검증 기준 + +| 검증 항목 | 확인 내용 | +| --------- | ------------------------------------------------- | +| 전체 건수 | 원본 API 조회 건수와 중앙 저장 건수 비교 | +| 원본 번호 | 일부 샘플의 원본 ID와 중앙 상세 연결 확인 | +| 제목·본문 | 긴 글, 특수문자, HTML, 줄바꿈 확인 | +| 작성자 | 이름·이메일·부서 매핑 확인 | +| 상태 | 원본 상태와 중앙 상태 변환 확인 | +| 일시 | 시간대 변환과 등록·수정 일시 확인 | +| 첨부파일 | 파일명·크기·checksum·다운로드 확인 | +| 재실행 | 같은 범위를 다시 실행해 중복이 생기지 않는지 확인 | + +## 11. 운영 체크리스트 + +### 배포 전 + +- [ ] 스테이징 테스트 완료 +- [ ] 환경별 동시 배포 잠금 획득 +- [ ] 배포 commit SHA와 세 이미지 digest 확인 +- [ ] 운영 필수 Secret 존재 여부 확인 +- [ ] `MYSQL_BIND_ADDRESS`가 승인된 내부 IP인지 확인 +- [ ] MySQL·Redis·R2 연결 Preflight 성공 +- [ ] 현재·목표 TypeORM migration과 Alembic revision 확인 +- [ ] 두 Migration의 실행 순서와 스키마 호환성 검토 +- [ ] 공유 `userfeedback` 전체 DB 백업 성공 +- [ ] 네이버웍스 드라이브 업로드와 checksum 확인 +- [ ] R2 versioning 또는 로컬 첨부파일 backup 상태 확인 +- [ ] 롤백할 이전 image digest와 복원할 backup 식별자 확인 +- [ ] DB 초기화 후 배포라면 새 ABC API Key와 `SECRETARY_ABC_API_KEY` 일치 확인 + +### 배포 후 + +- [ ] 모든 컨테이너가 `Up` 상태 +- [ ] MySQL host port가 승인된 IP에만 게시됨 +- [ ] Redis `PING`이 `PONG` 반환 +- [ ] TypeORM migration과 Alembic revision이 목표값과 일치 +- [ ] ABC UUID PK/FK와 Secretary UUID mapping 불변 조건 정상 +- [ ] API와 Secretary API health 정상 +- [ ] SSO 버튼 노출 및 기존 관리자 이메일 연결·로그인 정상 +- [ ] 프로젝트명·채널명 URL과 내부 UUID 조회 정상 +- [ ] 동적 workspace mapping 정상 +- [ ] 피드백 목록·상세 조회 정상 +- [ ] 상태·중요도·담당자 변경 정상 +- [ ] 내부 메모·댓글 저장 정상 +- [ ] 첨부파일 등록·다운로드 정상 +- [ ] 동일 Idempotency Key의 중복 생성 방지 정상 +- [ ] 외부 연동 오류 없음 +- [ ] 최근 로그에 재시작 반복이나 5xx 없음 +- [ ] commit SHA·image digest·revision·backup checksum 배포 기록 완료 + +### 현재 pipeline 개선 완료 조건 + +- [ ] CI 검증을 통과한 동일 commit 이미지가 staging과 production에 사용됨 +- [ ] 앱 시작과 DB Migration 실행이 분리됨 +- [ ] TypeORM 이후 Alembic이 한 번씩만 직렬 실행됨 +- [ ] Migration 전 backup·checksum 실패 시 배포가 중단됨 +- [ ] 앱은 `AUTO_MIGRATION=false`로 기동됨 +- [ ] 삭제된 소스 파일이 서버에 잔존하지 않는 release 교체 방식 적용 +- [ ] 실패 단계에 따라 앱 롤백과 DB 복원을 구분함 + +### 일회성 이관 전 + +- [ ] 원본 조회 API와 인증 방식 확정 +- [ ] 프로젝트·채널 매핑 확정 +- [ ] 필드와 상태값 매핑 확정 +- [ ] 첨부파일 다운로드 가능 여부 확인 +- [ ] Dry Run 완료 +- [ ] 스테이징 전체 이관 및 검증 완료 +- [ ] 운영 DB 백업 완료 +- [ ] 이관 전용 토큰 만료 시간 설정 +- [ ] 이관 후 토큰 폐기 계획 확인 diff --git a/docs/관리페이지 md 파일/uuidv7-first-distributed-design-and-task-list.md b/docs/관리페이지 md 파일/uuidv7-first-distributed-design-and-task-list.md new file mode 100644 index 0000000..7b18ed9 --- /dev/null +++ b/docs/관리페이지 md 파일/uuidv7-first-distributed-design-and-task-list.md @@ -0,0 +1,689 @@ +# UUID v7 기반 선제적 분산 설계 및 작업 목록 + +## 1. 목적 + +이 문서는 다음 방향으로 시스템을 처음부터 설계하기 위한 기준을 정의한다. + +- 핵심 도메인 테이블의 PK를 UUID v7 `BINARY(16)`으로 사용 +- 프로젝트별 작성 서버에서 중앙 DB 저장 전 ID 선발급 +- MySQL과 Redis는 Docker 내부 네트워크에서만 접근 +- 외부 API 재시도와 중복 등록을 Unique Key로 차단 +- 향후 API 수평 확장, Read Replica, 메시지 큐, DB 샤딩에 대응 + +현재 코드에는 공통 `INT AUTO_INCREMENT` PK, `ParseIntPipe`, `id: number` 타입, 프로젝트·채널·피드백 숫자 ID가 널리 사용되고 있다. 따라서 이 설계는 단순 컬럼 변경이 아니라 DB·API·프론트엔드·Docker·테스트를 함께 전환하는 신규 기준으로 적용한다. + +### 현재 전환 상태 + +UUID v7 생성·검증·`BINARY(16)` 변환 유틸리티, TypeORM Transformer/Pipe, 공통 Entity PK 전환을 적용했다. API 식별자와 웹 내부 타입은 UUID 문자열 계약을 유지한다. 관리 화면의 피드백 URL은 가독성을 위해 프로젝트명·채널명을 경로 alias로 사용하고 피드백 자체는 UUID를 사용한다. + +Node.js `24.14.1` 환경에서 API 전체 타입체크, API 단위 테스트 66개 suite·993건, API 배포용 SWC 빌드 762개 파일, 웹 전체 타입체크와 프로덕션 빌드 79개 페이지를 통과했다. 로컬 MySQL을 초기화한 뒤 migration을 순서대로 실행했고, 최종 migration 이력 59개와 모든 도메인 PK `BINARY(16)`을 확인했다. TypeORM의 `migrations.id`는 메타데이터 테이블이므로 변환 대상에서 제외한다. + +기존 단위·통합·E2E 테스트 코드에 남아 있던 숫자형 내부 ID fixture와 레거시 API 경로는 UUID v7·현행 REST 계약으로 전환했다. 실제 MySQL을 사용하는 OpenSearch 비활성 E2E는 12개 suite·129건, OpenSearch 활성 E2E는 2개 suite·27건, 웹 단위·계약 테스트는 3개 suite·5건, Secretary UUID/ABC 계약 테스트는 4건을 통과했다. + +최종 로컬 기동 점검에서는 시스템 Node 18만 보이는 셸에서도 `start-local.sh`가 `.nvmrc`의 Node 24.14.1을 자동 선택했다. API DB health와 Secretary API health는 HTTP 200, Web 루트는 로그인 경로로 HTTP 308, Redis는 `PONG`으로 응답했다. API 응답의 UUID v7 `x-correlation-id` 헤더도 실제 Fastify 요청으로 검증했다. + +--- + +## 2. 최종 설계 원칙 + +### 2.1 식별자 원칙 + +```text +모든 핵심 도메인 PK UUID v7, DB는 BINARY(16) +외부 API 식별자 PK와 동일한 UUID v7 +관리 화면 피드백 URL projectName + channelName + feedback UUID v7 +프로젝트·채널 관계 FK로 검증 +원본 시스템 중복 방지 source_namespace + source_record_id +요청 재시도 중복 방지 consumer + idempotency_key +Redis 보조 저장소, Unique 제약의 대체재가 아님 +``` + +UUID v7은 애플리케이션에서 생성한다. MySQL의 `AUTO_INCREMENT`나 Redis의 `INCR`를 전역 ID 생성기로 사용하지 않는다. + +별도의 `public_id` 컬럼은 두지 않는다. DB PK인 UUID v7을 API·웹훅·로그의 공통 리소스 식별자로 사용한다. 관리 화면 URL의 프로젝트명·채널명은 사람이 읽기 위한 경로 alias이며, 서버 요청과 권한 검증에는 alias로 해석한 실제 UUID PK를 사용한다. + +### 2.2 UUID 저장 방식 + +```sql +id BINARY(16) NOT NULL PRIMARY KEY +``` + +API와 애플리케이션에서는 표준 UUID 문자열을 사용하고, TypeORM Transformer에서 문자열과 `BINARY(16)`을 변환한다. + +```text +API: 0198f4c0-7a54-7abc-8b1e-9d2a4e5b6c7d +DB: 16-byte binary value +``` + +`CHAR(36)` 또는 `VARCHAR(36)`을 PK로 사용하지 않는다. UUID v7 기본 정렬 순서를 보존할 수 있도록 네트워크 바이트 순서로 저장한다. + +### 2.3 PK와 업무 식별자의 분리 + +UUID PK는 전역 객체 식별자다. 사람이 보는 업무 번호와 외부 원본 식별자는 별도로 관리한다. + +```text +feedback.id UUID v7 PK +feedback.display_code ABC-2026-000123 -- 선택적 업무 표시 번호 +feedback.source_namespace +feedback.source_record_id +``` + +`project_id + channel_id + feedback_id`를 조합해 새로운 PK를 만들지 않는다. UUID v7 자체가 전역적으로 유일하므로 피드백 PK는 `feedback_id` 하나로 충분하다. + +### 2.4 확정된 분산 경계 + +연간 피드백·댓글 합계 5만 건을 기준으로 다음을 확정한다. + +- 현재 `feedbacks`에는 `project_id`를 중복 저장하지 않는다. 프로젝트는 `feedbacks.channel_id → channels.project_id`로 해석한다. +- API는 요청의 `projectId`, `channelId`, `feedbackId`가 같은 계층인지 조회 시 반드시 검증한다. +- 향후 DB 샤딩 키는 `project_id`로 한다. 프로젝트가 테넌트보다 부하 분산 단위가 작고 API 권한 경계와 일치하기 때문이다. +- `source_namespace`는 영문 대문자·숫자·밑줄로 구성한 2~64자 코드로 발급한다. 예: `EGBIM_QA`, `TOVA_PROD`. +- 모든 RP는 `source_namespace` 안에서 `source_record_id`를 전역 유일하게 제공한다. 기존 시스템이 채널 범위 ID만 제공하면 namespace 자체를 `EGBIM_QA_CHANNEL_A`처럼 분리한다. +- Redis는 멱등성 빠른 차단, 분산 락, 레이트리밋에만 우선 사용한다. 업무 원본과 Unique 제약의 최종 권위는 MySQL이다. +- 연 5만 건 단계에서는 피드백 JSON Generated Column을 선제 생성하지 않는다. 반복 쿼리와 slow query 측정으로 대상 키가 확정될 때만 추가한다. + +namespace 등록 정보는 다음 논리 모델로 관리한다. 초기에는 배포 설정으로 등록하고, RP가 늘어날 때 동일 구조의 관리 테이블로 승격한다. + +```text +source_namespace + code varchar(64) unique + display_name varchar(255) + owner varchar(255) + environment enum(QA, STAGING, PROD) + is_active boolean +``` + +--- + +## 3. Project·Channel·Feedback 관계 설계 + +### 3.1 권장 관계 + +```text +Tenant + └─ Project + └─ Channel + └─ Feedback + └─ Comment +``` + +권장 FK: + +```text +projects.tenant_id → tenants.id +channels.project_id → projects.id +feedbacks.channel_id → channels.id +comments.feedback_id → feedbacks.id +``` + +정규화만 고려하면 `feedbacks`에는 `channel_id`만 저장하고 프로젝트는 채널을 통해 조회할 수 있다. + +### 3.2 분산 라우팅을 위한 project_id 보관 + +향후 프로젝트 단위 샤딩 또는 권한 필터링을 빠르게 적용하려면 `feedbacks.project_id`를 중복 보관할 수 있다. + +현재 연 5만 건 설계에서는 이 중복 컬럼을 적용하지 않는다. 아래 복합 FK는 단일 DB의 실제 부하 측정에서 프로젝트 직접 필터가 병목으로 확인되거나 샤딩 전환이 시작될 때 적용하는 선택 설계다. + +이 경우 단순히 애플리케이션에서만 일치 여부를 확인하지 않고, 다음 복합 FK로 채널과 프로젝트의 일치성을 DB에서도 검증한다. + +```sql +ALTER TABLE channels + ADD UNIQUE KEY uk_channels_project_id_id (project_id, id); + +ALTER TABLE feedbacks + ADD CONSTRAINT fk_feedback_project_channel + FOREIGN KEY (project_id, channel_id) + REFERENCES channels (project_id, id); +``` + +이 설계의 의미는 다음과 같다. + +- `feedbacks.project_id`: 샤드 라우팅·권한·프로젝트 목록 조회용 +- `feedbacks.channel_id`: 실제 접수 채널 FK +- `feedbacks.id`: 전역 객체 PK + +`project_id`와 `channel_id`는 PK를 구성하지 않는다. 두 컬럼은 소속 검증과 라우팅을 위한 관계 컬럼이다. + +### 3.3 API 경로의 의미 + +다음 경로는 리소스 식별자라기보다 소속 검증과 권한 범위를 표현한다. + +```text +/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId} +``` + +조회 조건은 다음처럼 구성한다. + +```sql +SELECT * +FROM feedbacks +WHERE id = :feedbackId + AND project_id = :projectId + AND channel_id = :channelId; +``` + +여기서 `(project_id, channel_id, feedback_id)`는 Unique Key로 만들지 않는다. `feedback_id`가 이미 PK이기 때문이다. + +--- + +## 4. Unique Key 설계 + +### 4.1 기본 원칙 + +Unique Key는 다음 세 가지를 구분해서 설계한다. + +1. **객체의 전역 식별**: UUID v7 PK +2. **업무상 중복 방지**: 프로젝트·채널·외부 원본 키 조합 +3. **HTTP 재시도 중복 방지**: 소비자·멱등 키 조합 + +서로 다른 목적의 값을 하나의 거대한 복합 Unique Key로 합치지 않는다. + +### 4.2 권장 Unique Key 목록 + +| 대상 | 권장 Unique Key | 목적 | +| -------------- | --------------------------------------------------- | ------------------------- | +| Tenant | `tenant_id` 또는 외부 SSO 테넌트 키 | 전역 테넌트 식별 | +| Project | `(tenant_id, project_code)` | 테넌트 내 프로젝트 식별 | +| Channel | `(project_id, channel_code)` | 프로젝트 내 채널 식별 | +| Feedback | `(source_namespace, source_record_id)` | 외부 원본 중복 방지 | +| Comment | `(feedback_id, source_namespace, source_record_id)` | 외부 댓글 중복 방지 | +| Issue | `(project_id, issue_key)` | 프로젝트 내 이슈 식별 | +| Feedback-Issue | `(feedback_id, issue_id)` | 다대다 연결 중복 방지 | +| Field | `(channel_id, field_key)` | 채널 내 필드 키 중복 방지 | +| Option | `(field_id, option_key)` | 필드 내 옵션 키 중복 방지 | +| Member | `(project_id, user_id, role_id)` | 프로젝트 권한 중복 방지 | +| Idempotency | `(consumer, idempotency_key)` | 동일 요청 재처리 방지 | +| External Issue | `(provider, project_id, external_issue_id)` | 외부 이슈 중복 방지 | +| Object Storage | `(storage_bucket, storage_key)` | 파일 객체 중복 방지 | + +### 4.3 Feedback 원본 키의 최종 권장안 + +가장 좋은 방식은 RP마다 `source_namespace`를 발급하고, 그 네임스페이스 안에서 원본 ID를 유일하게 관리하도록 계약하는 것이다. + +```text +source_namespace = RP_EGBIM +source_record_id = 502 +``` + +Unique Key: + +```sql +UNIQUE KEY uk_feedback_source + (source_namespace, source_record_id) +``` + +이렇게 하면 `project_id`나 `channel_id`를 Unique Key에 불필요하게 추가하지 않아도 된다. + +### 4.4 원본 ID의 범위가 좁은 경우 + +외부 RP가 원본 ID를 프로젝트 또는 채널별로만 유일하게 보장한다면 범위를 명시해야 한다. + +```text +프로젝트 범위: (source_namespace, project_id, source_record_id) +채널 범위: (source_namespace, channel_id, source_record_id) +``` + +둘 중 하나만 선택한다. 프로젝트와 채널이 모두 포함된 다음 구조는 원본 계약상 정말 필요한 경우에만 사용한다. + +```text +(source_namespace, project_id, channel_id, source_record_id) +``` + +기본 정책은 `source_namespace + source_record_id`로 하고, 레거시 RP에 한해서만 프로젝트 또는 채널 범위를 추가한다. + +### 4.5 Idempotency Key와 Redis + +Redis의 `SETNX`는 빠른 중복 요청 차단에 사용할 수 있지만 최종 정합성은 MySQL Unique Key가 보장해야 한다. + +```text +1. Redis SETNX idempotency:{consumer}:{key} +2. MySQL INSERT +3. MySQL Unique 충돌 시 기존 결과 조회 +4. 처리 결과를 Redis에 짧은 TTL로 캐시 +``` + +Redis 장애나 만료가 발생해도 MySQL Unique Key로 중복 저장이 발생하지 않아야 한다. + +--- + +## 5. 핵심 테이블 예시 + +```sql +CREATE TABLE projects ( + id BINARY(16) NOT NULL, + tenant_id BINARY(16) NOT NULL, + project_code VARCHAR(64) NOT NULL, + name VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_projects_tenant_code (tenant_id, project_code), + KEY idx_projects_tenant (tenant_id) +); + +CREATE TABLE channels ( + id BINARY(16) NOT NULL, + project_id BINARY(16) NOT NULL, + channel_code VARCHAR(64) NOT NULL, + name VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_channels_project_code (project_id, channel_code), + UNIQUE KEY uk_channels_project_id_id (project_id, id), + KEY idx_channels_project_created (project_id, created_at, id) +); + +CREATE TABLE feedbacks ( + id BINARY(16) NOT NULL, + channel_id BINARY(16) NOT NULL, + source_namespace VARCHAR(64) NULL, + source_record_id VARCHAR(255) NULL, + data JSON NOT NULL, + admin_first_read_at DATETIME(6) NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + deleted_at DATETIME(6) NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_feedback_source (source_namespace, source_record_id), + KEY idx_feedback_channel_list + (channel_id, deleted_at, created_at DESC, id DESC), + CONSTRAINT fk_feedback_channel + FOREIGN KEY (channel_id) REFERENCES channels (id) +); + +CREATE TABLE feedback_comments ( + id BINARY(16) NOT NULL, + feedback_id BINARY(16) NOT NULL, + source_namespace VARCHAR(64) NULL, + source_record_id VARCHAR(255) NULL, + content TEXT NOT NULL, + created_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_comment_source + (feedback_id, source_namespace, source_record_id), + KEY idx_comments_feedback_created (feedback_id, created_at, id), + CONSTRAINT fk_comment_feedback + FOREIGN KEY (feedback_id) REFERENCES feedbacks (id) +); +``` + +외부 원본이 없는 내부 생성 데이터에서는 `source_namespace`와 `source_record_id`를 NULL로 둘 수 있다. MySQL의 Unique Key는 NULL을 여러 건 허용하므로 내부 댓글의 정상적인 다건 등록을 막지 않는다. + +### 5.1 FK 마이그레이션 순서 + +UUID PK/FK 타입을 변경하거나 되돌릴 때는 참조 그래프를 기준으로 다음 순서를 지킨다. + +1. 쓰기를 중단하고 백업을 확인한다. +2. 자식 테이블의 FK를 가장 하위부터 삭제한다. 연결 테이블 → 댓글·첨부 → 피드백·이슈 → 채널 → 프로젝트 → 테넌트 순이다. +3. PK와 FK 컬럼을 모두 같은 `BINARY(16)` 정의로 변경한다. +4. 부모 테이블부터 PK/Unique Key를 생성한다. 테넌트 → 프로젝트 → 채널 → 피드백·이슈 → 댓글·첨부 → 연결 테이블 순이다. +5. 자식 방향으로 FK를 다시 생성한다. +6. orphan 행, PK/FK 타입 불일치, 인덱스 누락이 0건인지 검사한 뒤 쓰기를 재개한다. + +TypeORM migration의 `up`은 위 생성 순서를, `down`은 반대 순서를 사용한다. FK 이름은 migration에 명시하여 환경마다 자동 생성 이름이 달라지지 않게 한다. + +--- + +## 6. REST 리소스와 화면 URL 설계 + +현재 상세창 URL은 다음처럼 리소스 ID가 Query String에 들어간다. + +```text +/main/project/2/feedback?channelId=2&feedbackId=106&detailWindow=1 +``` + +UUID v7과 REST 규칙을 적용하면 리소스 식별자 또는 화면용 경로 alias를 경로에 배치하고, Query String은 필터·정렬·페이지네이션 같은 선택 조건에만 사용한다. + +### 5.1 화면 URL + +```text +피드백 목록 +/main/projects/{projectName}/channels/{channelName}/feedbacks + +피드백 상세 +/main/projects/{projectName}/channels/{channelName}/feedbacks/{feedbackId} +``` + +예시: + +```text +/main/projects/EGBIM/channels/Q&A/feedbacks/0198f4c2-7a54-7abc-8b1e-9d2a4e5b6c7f +``` + +프로젝트·채널 이름을 경로 alias로 사용하면 다음 효과가 있다. + +- 프로젝트·채널을 URL에서 바로 식별할 수 있음 +- 피드백은 변경되지 않는 UUID로 정확히 식별됨 +- `detailWindow=1` 같은 화면 제어용 플래그가 필요 없음 +- 새로고침·공유·뒤로 가기가 동일한 리소스를 가리킴 +- URL만 보고 프로젝트·채널·피드백 계층을 이해할 수 있음 + +`feedbackId`가 전역 식별자이고 화면 경로의 프로젝트명·채널명은 내부 `projectId`, `channelId`로 해석하여 소속과 권한을 검증한다. 프로젝트명은 전역 Unique, 채널명은 프로젝트 내 Unique 제약을 사용한다. 기존 UUID 화면 URL도 진입 호환용으로 유지하고 현재 이름 URL로 정규화한다. 이름 변경 후에는 새 이름이 canonical URL이 된다. + +### 5.2 Query String 사용 범위 + +Query String은 리소스 ID가 아닌 조회 조건에만 사용한다. + +```text +/main/projects/{projectName}/channels/{channelName}/feedbacks + ?status=OPEN + &sort=createdAt.desc + &cursor=... + &limit=20 +``` + +다음 항목은 Query String에 두지 않는다. + +```text +channelId=... +feedbackId=... +detailWindow=1 +``` + +### 5.3 REST API 경로 + +컬렉션은 복수 명사로 표현하고, 동작은 HTTP method로 표현한다. + +| Method | Path | 의미 | +| -------- | ---------------------------------------------------------------------------------------- | ---------------------------- | +| `GET` | `/api/projects/{projectId}/channels/{channelId}/feedbacks` | 피드백 목록 조회 | +| `POST` | `/api/projects/{projectId}/channels/{channelId}/feedbacks` | 피드백 생성 | +| `GET` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}` | 피드백 상세 조회 | +| `PATCH` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}` | 피드백 부분 수정 | +| `DELETE` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}` | 피드백 삭제 또는 소프트 삭제 | +| `GET` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments` | 댓글 목록 조회 | +| `POST` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/comments` | 댓글 생성 | +| `GET` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues` | 연결 이슈 목록 조회 | +| `PUT` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues/{issueId}` | 이슈 연결 | +| `DELETE` | `/api/projects/{projectId}/channels/{channelId}/feedbacks/{feedbackId}/issues/{issueId}` | 이슈 연결 해제 | + +UUID가 전역적으로 유일하고 API가 소속 검증을 내부에서 처리한다면 다음 짧은 URI를 canonical detail URI로 추가할 수도 있다. + +```text +GET /api/v1/feedbacks/{feedbackId} +PATCH /api/v1/feedbacks/{feedbackId} +``` + +초기에는 권한 경계를 명확히 유지하기 위해 프로젝트·채널이 포함된 scoped URI를 기본으로 사용한다. + +### 5.4 기존 API 전환 규칙 + +| 기존 형태 | 권장 형태 | +| ---------------------------------------- | ---------------------------------------------------------------------- | +| `POST /feedbacks/search` | `GET /feedbacks?query=...` 또는 복합 검색 시 `POST /feedback-searches` | +| `POST /feedbacks/{id}/issue/{issueId}` | `PUT /feedbacks/{id}/issues/{issueId}` | +| `DELETE /feedbacks/{id}/issue/{issueId}` | `DELETE /feedbacks/{id}/issues/{issueId}` | +| `POST /feedbacks-with-images` | `POST /feedbacks` multipart/form-data | +| `PUT /feedbacks/{id}` | 부분 수정은 `PATCH /feedbacks/{id}` | +| `DELETE /feedbacks` 대량 삭제 | 개별 `DELETE` 반복 또는 별도 bulk command | + +### 5.5 외부 계약 버전과 호환 기간 + +- 현재 외부 계약 버전은 OpenAPI `1.0.0`이며 실제 URL은 기존 호환을 위해 `/api` prefix를 유지한다. +- canonical 요청은 `POST /feedbacks`(JSON 또는 multipart)와 `PATCH /feedbacks/{id}`이다. +- `POST /feedbacks-with-images`와 피드백·댓글 수정 `PUT` alias는 2027-03-31까지 유지한다. +- 구형 alias 응답에는 `Deprecation: true`와 `Sunset: Wed, 31 Mar 2027 00:00:00 GMT`를 제공한다. +- 다음 호환 불가능 변경부터 URL 또는 media type에 명시적인 v2를 도입하고 OpenAPI major 버전을 함께 올린다. + +### 5.6 상세창 라우팅 + +상세창이 Sheet/Modal이어도 URL은 상세 리소스를 가리켜야 한다. + +```text +목록에서 피드백 클릭 + ↓ +URL을 /feedbacks/{feedbackId}로 변경 + ↓ +상세 Sheet 표시 + ↓ +뒤로 가기 시 목록 복귀 +``` + +따라서 `detailWindow=1`은 제거하고, 현재 라우트에 `feedbackId`가 있는지로 상세창 표시 여부를 판단한다. + +### 5.7 REST URL의 권한 검증 + +서버는 경로의 세 ID가 실제로 연결되어 있는지 확인해야 한다. + +```sql +SELECT f.id +FROM feedbacks f +JOIN channels c ON c.id = f.channel_id +WHERE f.id = :feedbackId + AND f.channel_id = :channelId + AND c.project_id = :projectId; +``` + +UUID가 추측하기 어렵다는 이유로 프로젝트 권한과 채널 접근 권한 검사를 생략하지 않는다. + +--- + +## 7. Docker 네트워크 및 포트 설계 + +### 6.1 포트 정책 + +Docker Compose에서 `ports`는 호스트로 포트를 publish하고, `expose`는 컨테이너 간 사용 포트를 문서화한다. 실제 외부 차단 경계는 내부 Docker network와 `ports` 미설정이다. + +| 서비스 | 운영 외부 공개 | Docker 내부 접근 | +| ------------- | ------------------ | -------------------- | +| Web/Nginx | 공개 | `web:3000` | +| API | 직접 공개하지 않음 | `api:4000` | +| Secretary API | 필요 경로만 공개 | `secretary-api:8010` | +| MySQL | 공개하지 않음 | `mysql:3306` | +| Redis | 공개하지 않음 | `redis:6379` | +| OpenSearch | 공개하지 않음 | `opensearch:9200` | + +API의 DB 연결 문자열은 호스트 포트가 아니라 서비스 DNS를 사용한다. + +```text +MYSQL_PRIMARY_URL=mysql://userfeedback:***@mysql:3306/userfeedback +REDIS_URL=redis://:***@redis:6379/0 +``` + +### 6.2 Compose 기본 구조 + +```yaml +services: + api: + ports: + - '4000:4000' # 로컬 개발에서만 필요 + expose: + - '4000' + networks: [frontend, backend] + + mysql: + expose: + - '3306' + networks: [backend] + + redis: + image: redis:7 + expose: + - '6379' + networks: [backend] + +networks: + frontend: + backend: + internal: true +``` + +운영 Compose에서는 MySQL과 Redis에 `ports`를 설정하지 않는다. 로컬에서 DB 클라이언트로 접속해야 하면 별도의 local override에서만 다음처럼 loopback에 바인딩한다. + +```yaml +ports: + - '127.0.0.1:13306:3306' +``` + +Redis는 외부에 공개하지 않는다. 캐시·락·멱등성 보조 용도로 사용하고 원본 데이터와 유일성 보장은 MySQL이 담당한다. + +### 6.3 Redis 사용 범위 + +Redis에 저장할 수 있는 데이터: + +- 목록 캐시 +- 짧은 TTL의 Idempotency 결과 +- Rate Limit 카운터 +- 분산 Lock +- 비동기 작업 큐의 임시 상태 + +Redis에 저장하면 안 되는 단일 원본: + +- 피드백 본문 +- 댓글 원문 +- 사용자 권한의 최종 상태 +- Unique Key를 대신하는 유일성 데이터 + +--- + +## 8. 구현 작업 목록 + +### Phase 0. 설계 확정 + +- [x] UUID v7의 canonical string과 `BINARY(16)` 변환 규칙 확정 +- [x] UUID v7 생성 라이브러리(`uuid` v13)와 기본 생성 방식 확정 +- [x] 모든 핵심 도메인 테이블의 UUID PK 적용 범위 확정 +- [x] `source_namespace` 발급 규칙과 등록 모델 설계 (2~64자 대문자·숫자·밑줄, 환경별 namespace) +- [x] `source_record_id`의 유일 범위를 namespace 전역으로 통일 (채널 범위 RP는 namespace 분리) +- [x] 현재 규모에서는 `project_id`를 feedbacks에 denormalize하지 않기로 확정 +- [x] 향후 샤드 라우팅 기준을 project로 확정 +- [x] 외부 API는 별도 `publicId` 없이 UUID PK를 `id`로 사용하고, 관리 화면 URL은 프로젝트명·채널명 alias와 피드백 UUID를 조합 +- [x] Redis는 멱등성 빠른 차단·분산 락·레이트리밋에 사용하고 MySQL을 최종 권위로 확정 + +### Phase 1. DB 스키마 및 마이그레이션 + +- [x] 대상 DB의 실제 PK·FK·인덱스·row 수 인벤토리 수집 (`scripts/uuid-migration-inventory.sql` 및 migration introspection) +- [x] 기존 `CommonEntity`의 increment PK 제거 +- [x] UUID v7 공통 PK 컬럼/Transformer 구현 (`apps/api/src/common/uuid-binary.transformer.ts`) +- [x] 프로젝트·채널·피드백·댓글·이슈 PK를 `BINARY(16)`으로 설계 +- [x] 모든 FK와 다대다 조인 테이블의 타입 통일 +- [x] `project_id + channel_id` 복합 FK는 현재 미적용으로 검증 (feedbacks에 project_id를 저장하지 않음) +- [x] `source_namespace + source_record_id` Unique Key 추가 +- [x] 댓글·외부 이슈·첨부파일의 원본 Unique Key 추가 +- [x] 목록 조회용 `(channel_id, deleted_at, created_at, id)` 인덱스 추가 +- [x] `feedback_issue` 연결 테이블에 `(feedback_id, issue_id)` PK 적용 +- [x] JSON Generated Column은 현 규모에서 보류하고 slow query로 반복 키가 확인될 때 추가하도록 설계 +- [x] DB 초기화용 schema dump와 TypeORM migration을 새 PK 기준으로 재생성 +- [x] FK 삭제 순서와 생성 순서를 문서화 + +### Phase 2. 애플리케이션 ID 계층 + +- [x] 점진적 전환이 가능한 `Uuid` branded 공통 ID 타입 도입 (런타임 경계는 UUID v7 검증 유지) +- [x] UUID v7 생성기 구현 (`apps/api/src/common/uuid-v7.ts`) +- [x] 문자열↔Buffer 변환 Transformer 구현 +- [x] UUID 형식 검증 Pipe/Validator 구현 (`apps/api/src/common/pipes/uuid-v7.pipe.ts`) +- [x] `ParseIntPipe`를 UUID 검증으로 교체 +- [x] `id: number`, `projectId: number`, `channelId: number` 타입 전환 +- [x] Entity factory의 숫자 ID 할당 코드 전환 +- [x] Map·Set·캐시 키 생성 규칙 전환 +- [x] UUID를 일반 JavaScript number로 변환하지 않도록 금지 규칙 추가 + +### Phase 3. API 및 외부 연동 + +- [x] API의 project/channel/feedback/comment/issue ID를 UUID로 전환 +- [x] 화면 목록 URL을 `/main/projects/{projectName}/channels/{channelName}/feedbacks`로 전환 +- [x] 화면 상세 URL을 `/main/projects/{projectName}/channels/{channelName}/feedbacks/{feedbackId}`로 전환 +- [x] 프로젝트명·채널명 URL을 실제 UUID로 해석하고 기존 UUID 화면 URL을 이름 URL로 정규화 +- [x] 이름 URL에서도 피드백 상세 권한 조회에는 실제 프로젝트 UUID를 전달해 중요도·삭제 등 수정 권한 유지 +- [x] canonical URL에서 `channelId`, `feedbackId`, `detailWindow` Query String 제거 (기존 URL은 진입 호환용으로 유지) +- [x] `open-detail-window`를 경로 기반 라우팅으로 변경 +- [x] 상세 Sheet/Modal 표시 여부를 `feedbackId` 라우트 존재 여부로 변경 +- [x] 목록 Query String은 필터·정렬·페이지 상태에만 사용 +- [x] API 경로의 `issue` 단수형을 `issues` 복수형으로 변경 (기존 단수형 경로는 호환 alias 유지) +- [x] `feedbacks-with-images`를 `POST /feedbacks` multipart 요청으로 통합 (기존 경로는 sunset alias) +- [x] 피드백·댓글 부분 수정 API를 `PATCH`로 통일 (기존 `PUT`은 sunset alias) +- [x] API DTO의 ID 예시와 OpenAPI schema를 UUID 문자열로 갱신 +- [x] 프로젝트·채널·피드백 소속 검증 쿼리 구현 +- [x] `source_namespace + source_record_id` 기반 중복 등록 처리 구현 +- [x] `consumer + idempotency_key` 기반 재시도 처리 구현 (Redis 선점 + MySQL Unique 최종 권위) +- [x] Webhook payload의 ID 형식과 상세 링크를 UUID canonical URL로 전환 +- [x] OpenSearch 문서 ID와 채널 색인명을 UUID 문자열로 전환 +- [x] Gitea/Jira/GitHub 외부 이슈 매핑 Unique Key와 provider 저장 적용 +- [x] Secretary API의 `abc_feedback_id` 형식과 DB 모델 전환 (UUID v7 검증 및 Alembic 0020/0021) +- [x] Web 지원 BFF에서 UUID 피드백 ID를 Secretary 내부 숫자 티켓에 안전하게 연결하고 `/tickets/NaN` 요청 제거 +- [x] Web의 로컬 workspace 매핑이 누락되어도 상태 목록은 Secretary의 DB 매핑으로 fallback하도록 보강 +- [x] Web의 `SUPPORT_*_PROJECT_ID`·`SUPPORT_*_CHANNEL_ID` 고정 환경변수를 제거하고 ABC 내부 API 기반 동적 workspace 매핑(30초 캐시)으로 전환 +- [x] 외부 클라이언트 계약 버전과 호환 기간 정의 (OpenAPI 1.0.0, legacy sunset 2027-03-31) +- [x] REST URL 공유·새로고침·상세창 복귀 계약 테스트 추가 (`open-detail-window.spec.ts`, canonical route 프로덕션 빌드 검증) + +### Phase 4. Redis 도입 + +- [x] Redis Compose 서비스 추가 +- [x] 운영 Compose에서 Redis `ports` 제거, `expose: 6379`만 사용 +- [x] Redis 전용 내부 네트워크 연결 +- [x] Redis 비밀번호/ACL을 Secret 또는 환경변수로 관리 +- [x] Redis healthcheck 추가 +- [x] `REDIS_URL` 설정과 NestJS Redis 모듈 추가 +- [x] 캐시 키 네이밍 규칙 정의 (`abc:v1:{feature}:{kind}:{consumer}:{key}`) +- [x] Idempotency 키 TTL과 결과 보존 정책 정의 (lock 30초, 결과 24시간, MySQL 30일) +- [x] Redis 장애 시 MySQL만으로 처리 가능한 fallback 구현 +- [x] Redis를 Unique Key의 최종 권위로 사용하지 않는 테스트 추가 (Redis 중지 상태 통합 테스트 통과) + +### Phase 5. Docker 및 배포 + +- [x] MySQL 운영 Compose의 host `ports` 제거 +- [x] MySQL `expose: 3306` 추가 +- [x] Redis `expose: 6379` 추가 +- [x] API·Secretary API의 DB 주소를 `mysql:3306`으로 통일 +- [x] API·Secretary API의 Redis 주소를 `redis:6379`로 통일 +- [x] `frontend`·`backend` network 분리 +- [x] backend network에 `internal: true` 적용 +- [x] 로컬 전용 Compose override에만 데이터 저장소 `127.0.0.1` 포트 바인딩 +- [x] MySQL·Redis healthcheck와 `depends_on` 조건 추가 +- [x] 운영 비밀번호와 토큰을 Compose 파일에서 제거 (필수 환경변수 주입) +- [x] Gitea 배포 workflow에서 MySQL·Redis 비밀값을 검증하고 URL-encoding한 내부 DB/Redis URL을 동적으로 생성 +- [x] 백업 볼륨과 복구 절차 문서화 + +### Phase 6. 테스트 및 검증 + +- [x] UUID v7 생성 충돌 테스트 +- [x] 같은 밀리초 동시 생성 테스트 +- [x] UUID binary round-trip 테스트 +- [x] PK/FK 저장·조회 테스트 (로컬 migration 후 PK/FK 타입 불일치 0건) +- [x] project/channel/feedback 소속 불일치 차단 테스트 +- [x] 동일 source record 재전송 테스트 (통합 DB에서 동일 UUID 반환 및 1건 유지 검증) +- [x] 동일 idempotency key 재전송 테스트 (동일 UUID·DB 1건, 다른 payload 409) +- [x] Redis 장애·재시작 시 중복 저장 방지 테스트 (실제 컨테이너 중지/재시작 검증) +- [x] API 전 엔드포인트 UUID 입력 테스트 (controller param 정적 계약 + OpenAPI path schema 검사) +- [x] OpenSearch·Webhook·Secretary 연동 테스트 (OpenSearch 활성 27건, Secretary 계약 5건) +- [x] 지원 상세 UUID 회귀 테스트 (상태·상세·담당자·내부 메모·댓글·읽음 처리 실제 HTTP 6건 모두 200) +- [x] 프로젝트명·채널명 피드백 URL과 기존 UUID URL의 HTTP 200 및 라우팅 단위 테스트 검증 +- [x] workspace 코드↔ABC 프로젝트·채널 UUID 동적 매핑 단위 테스트 추가 +- [x] 프로젝트명 URL 전환 후 중요도 선택이 read-only가 되지 않도록 권한 조회 회귀 수정 +- [x] 중요도 수정 시 읽기 전용 요청자 메타데이터를 PATCH payload에서 제외하고 `MEDIUM → HIGH → MEDIUM` 실제 저장·원복 검증 +- [x] UUID 피드백 상세 조회에 Secretary 담당자 정보를 병합하고 `지정 해제 → 재지정` 실제 저장·재조회·원복 검증 +- [x] integration/E2E fixture·seed 데이터 UUID 전환 및 실제 실행 검증 (OpenSearch 비활성 12 suite·129건 통과) +- [x] 레거시 API E2E 경로를 현행 `/admin/...` 및 project-scoped API 계약으로 재작성 +- [x] OpenSearch 활성·비활성 모드별 E2E provider와 검증 분리 +- [x] 페이지네이션 cursor가 `(created_at, id)`를 안정적으로 처리하는지 검증 (동일 시각 UUID tie-break 포함) +- [x] 연간 5만~10만 건 데이터셋과 피크 동시 목록 쿼리 부하 테스트 (10만 행·동시 32 연결 결과 runbook 기록) + +### Phase 7. 운영 준비 + +- [x] UUID·source key·idempotency key 로깅 정책 확정 +- [x] 개인정보와 원본 데이터가 로그에 노출되지 않도록 마스킹 +- [x] DB Unique 충돌률 모니터링 (`abc_db_unique_conflicts_total`) +- [x] Redis 메모리·eviction·연결 수 모니터링 기준 정의 +- [x] MySQL CPU·I/O·Buffer Pool·Lock wait 모니터링 기준 정의 +- [x] API p95/p99와 재시도율 모니터링 (`abc_http_request_duration_seconds`, idempotency outcome) +- [x] UUID 기반 장애 추적용 correlation ID 적용 (Fastify 실제 HTTP 응답 헤더 및 adapter 호환 단위 테스트 검증) +- [x] MySQL 복구와 Redis 재구축 절차 검증 (33 tables·59 migrations·row mismatch 0) +- [x] 샤드 라우팅 기준과 확장 절차 문서화 + +--- + +## 9. 완료 기준 + +다음 조건을 만족하면 UUID v7 선제 전환 설계가 완료된 것으로 본다. + +- 핵심 도메인 테이블의 PK/FK가 모두 UUID v7 `BINARY(16)`이다. +- `project_id`, `channel_id`는 관계·라우팅 컬럼이고 PK 조합에 중복 포함하지 않는다. +- 외부 원본 중복은 `source_namespace + source_record_id`로 차단한다. +- HTTP 재시도 중복은 MySQL Unique Key와 Redis 보조 처리로 차단한다. +- MySQL과 Redis는 운영 환경에서 호스트에 직접 publish되지 않는다. +- Redis가 없어도 원본 데이터의 정합성은 MySQL만으로 유지된다. +- API·프론트엔드·웹훅·검색 색인이 UUID 형식을 일관되게 사용한다. +- 피드백 상세 URL에 프로젝트명·채널명과 `feedbackId`가 경로로 표시되고 `detailWindow` Query String이 없다. +- Query String은 필터·정렬·페이지네이션에만 사용된다. +- API가 복수 명사와 HTTP method 중심의 REST 규칙을 따른다. +- 연간 5만~10만 건 및 피크 부하 테스트를 통과한다. diff --git a/docs/관리페이지 md 파일/관리페이지-개념설명.md b/docs/관리페이지 md 파일/관리페이지-개념설명.md new file mode 100644 index 0000000..48c4629 --- /dev/null +++ b/docs/관리페이지 md 파일/관리페이지-개념설명.md @@ -0,0 +1,316 @@ +# 관리페이지 개념 설명 + +> 운영팀이 관리페이지의 구조와 용어를 처음 접할 때 읽는 문서로 정리 +> +> 실제 처리 순서는 [관리페이지 운영팀 사용 가이드](<./관리페이지-운영팀-사용가이드.md>)에서 확인 + +## 1. 관리페이지가 하는 일 + +여러 서비스에서 들어온 Q&A를 한곳에 모아 확인하고 처리하는 화면으로 정리 + +사용자가 남긴 질문이나 요청을 운영팀이 읽고, 담당자를 정하고, 답변하고, 필요한 개발 작업까지 연결할 수 있도록 구성 + +```mermaid +flowchart LR + A[서비스에서 Q&A 작성] --> B[관리페이지에서 피드백으로 확인] + B --> C[답변 또는 내부 처리] + C --> D[필요 시 이슈 연결] + D --> E[처리 결과 안내] +``` + +## 2. Q&A와 피드백 + +Q&A와 피드백은 서로 다른 글이 아니라 같은 요청을 화면별로 다르게 부르는 이름으로 구분 + +| 사용하는 곳 | 부르는 이름 | 의미 | +| --- | --- | --- | +| 사용자가 글을 작성하는 화면 | Q&A | 사용자가 남긴 질문·요청·불편 사항 | +| 운영팀이 글을 처리하는 화면 | 피드백 | 관리 대상이 된 Q&A | + +```text +사용자 화면: Q&A + ↓ +관리페이지: 피드백 +``` + +따라서 운영팀은 “새 피드백이 들어왔다”고 말하지만, 실제로는 사용자가 작성한 Q&A가 관리페이지에 표시된 것으로 이해 + +## 3. 역할 구분 + +관리페이지와 Q&A 화면에서 역할을 다음과 같이 구분 + +| 역할 | 역할의 의미 | 주요 관심사 | +| --- | --- | --- | +| 시스템관리자 | 전체 시스템과 여러 프로젝트를 관리하는 역할 | 전체 현황, 프로젝트별 업무 확인 | +| 프로젝트 담당자(읽는 당사자) | 맡은 프로젝트의 Q&A를 읽고 처리하는 역할 | 담당자 지정, 답변, 내부 협업, 이슈 처리 | +| Q&A 작성자 | 서비스에서 질문이나 요청을 남긴 사람 | Q&A 작성, 답변 확인, 추가 문의 | + +권한에 따라 확인할 수 있는 프로젝트와 실행할 수 있는 작업이 달라질 수 있도록 구성 + +## 4. 프로젝트와 채널 + +### 4.1 프로젝트 + +프로젝트는 하나의 서비스나 업무 단위를 묶는 가장 큰 관리 단위 + +예시 + +- 사내 시스템 +- 인트라넷 Q&A +- 특정 업무 서비스 + +### 4.2 채널 + +채널은 프로젝트 안에서 Q&A를 받는 세부 접수 창구 + +하나의 프로젝트에 여러 채널을 둘 수 있고, 문의 종류를 나누는 기준으로 사용 + +예시 + +```text +인트라넷 Q&A 프로젝트 +├── 계정 문의 채널 +├── 권한 문의 채널 +└── 시스템 오류 채널 +``` + +### 4.3 프로젝트와 채널의 관계 + +| 구분 | 프로젝트 | 채널 | +| --- | --- | --- | +| 범위 | 큰 서비스·업무 묶음 | 프로젝트 안의 세부 접수 창구 | +| 확인 질문 | 어느 서비스의 요청인가? | 어떤 종류의 요청인가? | +| 관리페이지 열 | 프로젝트 | 채널 | +| 예시 | 인트라넷 Q&A | 계정 문의 | + +Q&A는 반드시 하나의 프로젝트와 채널을 기준으로 관리되도록 구성 + +```text +프로젝트 +└── 채널 + └── Q&A + └── 피드백 +``` + +프로젝트와 채널을 먼저 확인해야 담당자와 처리 방향을 잘못 정하지 않음 + +## 5. 피드백과 이슈 + +### 5.1 피드백 + +피드백은 사용자의 Q&A 자체를 처리하는 항목 + +주로 다음 내용을 관리 + +- 사용자의 질문과 요청 +- 작성자 정보 +- 중요도 +- 담당자 +- 댓글/답변 +- 담당자 내부 메모 +- 연결된 이슈 +- 피드백 상태 + +### 5.2 이슈 + +이슈는 피드백을 해결하기 위해 필요한 개발·수정·확인 작업 + +예시 + +- 로그인 오류 수정 +- 화면 문구 변경 +- 권한 확인 및 수정 +- 특정 조건에서 발생하는 오류 조사 + +### 5.3 연결 관계 + +한 피드백에 이슈가 없을 수도 있고, 여러 이슈가 연결될 수도 있도록 구성 +같은 원인으로 들어온 여러 피드백을 하나의 이슈에 연결할 수도 있도록 구성 + +```mermaid +flowchart LR + F1[피드백 A] --> I[이슈: 로그인 오류 수정] + F2[피드백 B] --> I + F3[피드백 C] --> I +``` + +이슈를 연결했다고 피드백이 자동으로 완료되는 것은 아니므로 각각의 처리 상태를 따로 확인 + +## 6. 피드백 상태와 이슈 상태 + +피드백 상태는 사용자의 요청이 어디까지 처리되었는지 나타내고, 이슈 상태는 개발 작업이 어디까지 진행되었는지 나타내도록 분리 + +| 상태 | 공통 의미 | +| --- | --- | +| 신규 | 아직 본격적으로 확인하지 않은 상태 | +| 접수 | 내용을 확인하고 처리 대상으로 받은 상태 | +| 진행중 | 답변·확인·개발 작업을 진행하는 상태 | +| 완료 | 필요한 처리와 안내가 끝난 상태 | +| 보류 | 일정·추가 정보·외부 사유로 잠시 멈춘 상태 | + +상태 흐름은 다음처럼 이해 + +```text +신규 → 접수 → 진행중 → 완료 + ↘ 보류 +``` + +보류는 영구 종료가 아니라 다시 처리할 수 있도록 잠시 멈춘 상태로 구분 +보류로 변경할 때는 보류 사유를 함께 남기는 방식으로 정리 + +### 상태를 따로 관리하는 이유 + +| 상황 | 피드백 상태 | 이슈 상태 | +| --- | --- | --- | +| 고객에게 답변했고 개발 작업이 없음 | 완료 | 해당 없음 | +| 개발 작업을 시작함 | 진행중 | 진행중 | +| 개발은 끝났지만 고객 안내가 남음 | 진행중 | 완료 | +| 고객 안내까지 끝남 | 완료 | 완료 | + +개발 이슈가 완료된 뒤에도 피드백에 결과를 답변하고 피드백 상태를 완료로 바꾸는 작업이 필요 + +### 6.1 핵심 처리 순서 + +세부 자동화 동작은 제외하고, 운영팀이 기억할 핵심 순서만 다음과 같이 정리 + +```mermaid +flowchart TD + A[사용자가 Q&A 작성] --> B[관리페이지에 피드백 등록
상태: 신규] + B --> C[담당자 지정 및 내용 확인
상태: 접수] + C --> D{개발 작업이 필요한가?} + D -- 아니오 --> E[댓글/답변 작성] + D -- 예 --> F[이슈 생성 또는 기존 이슈 연결] + F --> G[이슈 처리
상태: 진행중] + G --> H[이슈 완료 여부 확인] + H --> E + E --> I[처리 완료 안내] + I --> J{작성자 확인 또는 확인 기간 만료} + J --> K[피드백 완료] +``` + +### 순서도 읽는 법 + +1. Q&A가 등록되면 관리페이지에서 피드백 `신규`로 확인 +2. 담당자가 내용을 처음 확인하면 `접수`로 처리 +3. 개발 작업이 없으면 답변 후 완료 안내를 진행 +4. 개발 작업이 있으면 이슈를 연결하고, 이슈가 끝난 뒤 답변과 완료 안내를 진행 +5. 작성자가 완료를 확인하거나 확인 기간이 지나면 피드백을 `완료`로 처리 + +### 꼭 기억할 예외 + +- 처리하기 어려운 사유가 있으면 `보류`로 바꾸고 사유를 남겼음. +- 보류 중인 항목은 댓글이나 외부 상태 변경만으로 자동 진행하지 않았음. +- 완료 안내 뒤 작성자가 다시 문의하면 피드백을 다시 확인 +- 완료된 이슈와 피드백은 각각 별도로 완료 처리 + +## 7. 댓글과 담당자 내부 메모 + +두 입력 영역은 읽는 사람이 다르므로 목적을 분리 + +| 구분 | 댓글/답변 | 담당자 내부 메모 | +| --- | --- | --- | +| 읽는 사람 | Q&A 작성자와 운영팀 | 운영팀과 관리자 | +| 목적 | 고객에게 처리 결과를 안내 | 운영팀끼리 내용을 공유 | +| 작성 예시 | 확인 결과, 조치 내용, 추가 안내 | 재현 방법, 원인 추정, 인수인계 | +| 작성 시 주의 | 고객이 이해할 수 있게 작성 | 고객에게 공개되면 안 되는 내용도 작성 가능 | + +고객에게 전달할 내용은 댓글/답변에 작성하고, 내부 협의 내용은 담당자 내부 메모에 작성하도록 구분 + +## 8. 담당자와 중요도 + +### 담당자 + +담당자는 피드백이나 이슈를 실제로 확인하고 처리할 사람 + +담당자가 지정되지 않은 항목은 처리 주체가 불명확해질 수 있으므로 확인 후 담당자를 지정 + +### 중요도 + +중요도는 먼저 처리해야 하는 정도를 나타내는 정보 + +중요도가 높거나 오래 업데이트되지 않은 피드백부터 확인하면 처리 우선순위 판단에 도움 + +## 9. Gitea 이슈 + +Gitea 이슈는 개발자가 실제 수정 작업을 진행하는 외부 개발 작업 항목 + +관리페이지의 이슈와 Gitea 이슈를 연결하면 운영팀이 개발 작업의 번호와 상태를 함께 확인 가능 + +```text +관리페이지 이슈 + ↕ 연결 +Gitea 개발 이슈 +``` + +기존 Gitea 이슈가 있으면 검색해 연결하고, 관련 작업이 없으면 새 개발 이슈를 생성해 연결 + +Gitea 연결 여부와 피드백 완료 여부는 별개이므로 개발 이슈를 연결한 뒤에도 고객 답변과 피드백 상태를 확인 + +## 10. 목록의 주요 정보 + +### 피드백 처리 목록 + +피드백 처리 목록은 사용자의 Q&A를 찾아 답변하고 처리하는 데 필요한 정보를 표시하도록 구성 + +| 항목 | 의미 | +| --- | --- | +| 프로젝트 / 채널 | 요청이 들어온 서비스와 접수 창구 | +| 제목 | 피드백의 제목과 상세 진입점 | +| 작성자 | Q&A를 남긴 사람 | +| 담당자 | 현재 처리하는 사람 | +| 피드백 상태 | 고객 요청의 처리 단계 | +| 중요도 | 우선 처리 정도 | +| 최근 업데이트 | 마지막으로 변경된 시점 | +| 연결 이슈 | 관련 개발 작업 | +| 등록일시 | Q&A가 등록된 시점 | + +### 이슈 처리 목록 + +이슈 처리 목록은 개발 작업을 찾고 연결된 피드백까지 확인하도록 구성 + +| 항목 | 의미 | +| --- | --- | +| 프로젝트 / 채널 | 이슈가 속한 서비스와 접수 창구 | +| 제목 | 이슈의 제목과 상세 진입점 | +| 담당자 | 이슈를 처리하는 사람 | +| 이슈 상태 | 개발 작업의 처리 단계 | +| 최근 업데이트 | 이슈가 마지막으로 변경된 시점 | +| 연결된 피드백 | 해당 이슈와 관련된 Q&A | +| 이슈 등록일시 | 이슈가 등록된 시점 | + +연결 항목이 여러 개이면 대표 항목 뒤에 `외 N개`로 표시 + +## 11. 한눈에 보는 전체 구조 + +```mermaid +flowchart TD + P[프로젝트] --> C[채널] + C --> Q[Q&A] + Q --> F[피드백] + F --> R[댓글/답변] + F --> M[담당자 내부 메모] + F -. 필요 시 연결 .-> I[관리페이지 이슈] + I -. 개발 작업 연결 .-> G[Gitea 이슈] +``` + +정리하면 다음 구조로 이해 + +```text +프로젝트 +└── 채널 + └── Q&A = 피드백 + ├── 담당자 + ├── 중요도 + ├── 댓글/답변 + ├── 담당자 내부 메모 + └── 이슈 ── Gitea 이슈 +``` + +## 12. 핵심만 다시 정리 + +- Q&A는 사용자가 작성한 글이고, 피드백은 관리페이지에서 부르는 이름 +- 프로젝트는 큰 서비스 단위이고, 채널은 프로젝트 안의 세부 접수 창구 +- 피드백은 고객 요청을 처리하는 항목이고, 이슈는 개발 작업을 처리하는 항목으로 나누었음. +- 댓글/답변은 고객에게 공개되고, 담당자 내부 메모는 운영팀끼리만 공유 +- 피드백 상태와 이슈 상태는 서로 영향을 주지만 자동으로 합쳐지지 않도록 분리 +- 이슈가 완료되어도 고객 답변과 피드백 상태 변경을 별도로 처리 diff --git a/docs/관리페이지 md 파일/관리페이지-운영팀-사용가이드.md b/docs/관리페이지 md 파일/관리페이지-운영팀-사용가이드.md new file mode 100644 index 0000000..f0deef5 --- /dev/null +++ b/docs/관리페이지 md 파일/관리페이지-운영팀-사용가이드.md @@ -0,0 +1,327 @@ +# 관리페이지 운영팀 사용 가이드 + +> Q&A를 확인하고, 답변하고, 필요한 개발 작업까지 연결하는 업무 안내서입니다. + +## 1. 이 문서에서 먼저 알아둘 것 + +사용자가 남긴 글을 사용자 화면에서는 **Q&A**, 관리페이지에서는 **피드백**이라고 부릅니다. +둘은 서로 다른 글이 아니라 같은 요청을 가리키는 이름입니다. + +```mermaid +flowchart LR + A[사용자: Q&A 작성] --> B[관리페이지: 피드백으로 확인] + B --> C[담당자 지정 및 검토] + C --> D{개발 작업이 필요한가?} + D -- 아니오 --> E[답변 작성 및 피드백 처리] + D -- 예 --> F[이슈 생성 또는 연결] + F --> G[이슈 처리 및 진행 상황 확인] + G --> E +``` + +### 핵심 원칙 + +- 피드백 상태와 이슈 상태는 **따로** 관리합니다. +- 고객에게 보여줄 내용은 **댓글/답변**에 작성합니다. +- 운영팀끼리만 공유할 내용은 **담당자 내부 메모**에 작성합니다. +- 이슈를 연결했다고 피드백이 자동으로 완료되는 것은 아닙니다. +- 처리할 때는 제목보다 먼저 **프로젝트와 채널**을 확인합니다. + +## 2. 역할별로 하는 일 + +| 역할 | 주로 하는 일 | 이 가이드에서 볼 내용 | +| --- | --- | --- | +| 시스템관리자 | 전체 프로젝트의 운영 현황 확인, 필요 시 업무 처리 | 통합 관리, 피드백 처리, 이슈 처리 | +| 프로젝트 담당자(읽는 당사자) | 맡은 프로젝트의 Q&A를 읽고 담당·답변·이슈 처리 | 피드백 처리, 이슈 처리 | +| Q&A 작성자 | Q&A 작성, 답변 확인, 추가 문의 | 사용자에게 공개되는 답변의 의미 | + +권한에 따라 보이는 프로젝트와 할 수 있는 작업이 다를 수 있습니다. 보이지 않는 프로젝트나 버튼이 있으면 시스템관리자에게 확인합니다. + +## 3. 용어를 쉽게 이해하기 + +| 화면 용어 | 쉬운 뜻 | +| --- | --- | +| 프로젝트 | 서비스 또는 업무 단위입니다. 예: 특정 사내 시스템, 인트라넷 업무 | +| 채널 | 프로젝트 안의 Q&A 접수 창구입니다. 예: 일반 문의, 장애 문의, 기능 개선 | +| Q&A | 사용자가 작성한 질문·요청·불편 사항입니다. | +| 피드백 | 관리페이지에서 처리하는 Q&A입니다. | +| 담당자 | 해당 피드백이나 이슈를 실제로 확인하고 처리하는 사람입니다. | +| 댓글/답변 | Q&A 작성자에게 공개되는 안내입니다. | +| 담당자 내부 메모 | 운영팀만 보는 협업 메모입니다. Q&A 작성자에게 보이지 않습니다. | +| 이슈 | 개발·수정·확인이 필요한 작업 항목입니다. | +| 연결된 이슈 | 피드백과 관련된 작업 항목입니다. 하나의 피드백에 여러 이슈를 연결할 수 있습니다. | +| Gitea 이슈 | 개발자가 작업하는 외부 개발 이슈입니다. 관리페이지의 이슈와 연결해 진행 상황을 확인할 수 있습니다. | +| 중요도 | 먼저 처리해야 하는 정도입니다. | +| 최근 업데이트 | 피드백 또는 이슈가 마지막으로 변경된 시점입니다. | + +## 4. 프로젝트 > 채널 구조 + +관리페이지의 데이터는 다음처럼 정리됩니다. + +```text +프로젝트 +└── 채널 + └── Q&A 1건 + └── 피드백으로 관리 + ├── 댓글/답변 + ├── 담당자 내부 메모 + └── 연결된 이슈 +``` + +### 프로젝트와 채널의 차이 + +| 구분 | 프로젝트 | 채널 | +| --- | --- | --- | +| 의미 | 큰 서비스·업무 묶음 | 그 서비스 안의 세부 접수 창구 | +| 예시 | 인트라넷 Q&A | 계정 문의, 권한 문의, 시스템 오류 | +| 운영 목적 | 어느 서비스의 요청인지 구분 | 어떤 종류의 요청인지 구분 | +| 목록에서 확인 | 프로젝트 열 | 채널 열 | + +예를 들어 사용자가 `인트라넷 Q&A > 계정 문의`에서 글을 남기면, 관리페이지에는 다음 정보로 표시됩니다. + +```text +프로젝트: 인트라넷 Q&A +채널: 계정 문의 +제목: 비밀번호를 잊어버렸습니다 +``` + +같은 프로젝트라도 채널이 다르면 문의 성격과 담당 부서가 다를 수 있습니다. 따라서 담당자를 지정하거나 이슈를 연결하기 전에 프로젝트와 채널을 확인합니다. + +## 5. 화면 구성과 목록 읽는 법 + +통합 관리 화면은 여러 프로젝트의 처리할 일을 한곳에서 보여줍니다. + +```text +통합 관리 +├── 피드백 처리: 사용자의 Q&A를 읽고 답변하는 곳 +└── 이슈 처리: 개발 작업과 연결된 이슈를 관리하는 곳 +``` + +### 피드백 처리 목록 + +주요 열은 다음 순서로 읽으면 됩니다. + +| 항목 | 확인할 내용 | +| --- | --- | +| # | 목록 순번입니다. | +| 프로젝트 / 채널 | 어느 서비스의 어떤 접수 창구인지 확인합니다. | +| 제목 | 피드백 상세를 여는 버튼입니다. | +| 작성자 | Q&A를 남긴 사람입니다. | +| 담당자 | 현재 처리 담당자입니다. `-`이면 아직 지정되지 않았습니다. | +| 피드백 상태 | 고객 요청의 처리 단계입니다. | +| 중요도 | 우선 처리 정도입니다. | +| 최근 업데이트 | 마지막 변경 후 며칠이 지났는지 확인합니다. | +| 연결 이슈 | 관련 개발 이슈입니다. 여러 개면 대표 제목 뒤에 `외 N개`로 표시됩니다. | +| 등록일시 | Q&A가 등록된 날짜와 시간입니다. | + +### 이슈 처리 목록 + +| 항목 | 확인할 내용 | +| --- | --- | +| # | 목록 순번입니다. | +| 프로젝트 / 채널 | 이슈가 속한 서비스와 채널입니다. | +| 제목 | 이슈 상세를 여는 버튼입니다. | +| 담당자 | 이슈를 맡은 사람입니다. | +| 이슈 상태 | 개발 작업의 처리 단계입니다. | +| 최근 업데이트 | 이슈가 마지막으로 변경된 후 며칠이 지났는지 확인합니다. | +| 연결된 피드백 | 이 이슈와 관련된 Q&A입니다. 여러 개면 대표 제목 뒤에 `외 N개`로 표시됩니다. | +| 이슈 등록일시 | 이슈가 등록된 날짜와 시간입니다. | + +### 검색과 필터 + +- 프로젝트 선택: 특정 프로젝트만 봅니다. +- 상태 버튼: 원하는 상태만 봅니다. 여러 상태를 함께 선택할 수 있습니다. +- 날짜 검색: 선택한 기간의 항목만 봅니다. +- 검색창: 제목·내용·작성자 또는 연결된 피드백을 검색합니다. +- `완료 상태 표시`, `보류 상태 표시`: 기본 목록에서 숨겨진 완료·보류 항목을 함께 봅니다. +- `내 담당`: 현재 로그인한 담당자의 피드백만 봅니다. + +목록에 항목이 보이지 않으면 먼저 프로젝트, 날짜, 상태 필터와 완료·보류 표시 여부를 확인합니다. + +## 6. 피드백 처리 방법 + +피드백 처리는 “사용자의 요청을 읽고 답변하는 일”입니다. + +### 기본 처리 순서 + +```mermaid +flowchart TD + A[피드백 목록 확인] --> B[프로젝트·채널 확인] + B --> C[제목을 눌러 상세 열기] + C --> D[작성자·내용·첨부파일 확인] + D --> E[담당자 지정] + E --> F[내부 메모로 운영팀 공유] + F --> G{개발 작업 필요?} + G -- 아니오 --> H[댓글/답변 작성] + G -- 예 --> I[기존 이슈 연결 또는 새 이슈 생성] + I --> H + H --> J[피드백 상태 변경] + J --> K[최근 업데이트 확인 후 종료] +``` + +### 단계별 안내 + +1. **목록에서 피드백을 찾습니다.** + - 프로젝트·채널·날짜·상태를 확인합니다. + - 오래된 미처리 건은 `최근 업데이트`와 중요도를 함께 봅니다. + +2. **제목을 눌러 상세를 엽니다.** + - 작성자, 이메일, 연락처, 소속, 직급, 등록일시, 수정일시, UUID를 확인할 수 있습니다. + - 본문, 중요도, 첨부파일, 연결된 이슈를 확인합니다. + +3. **담당자를 지정합니다.** + - 실제로 확인할 사람을 선택합니다. + - 담당자가 없으면 후속 조치가 누락될 수 있으므로 확인 후 지정합니다. + +4. **필요하면 내부 메모를 남깁니다.** + - 재현 방법, 확인할 부서, 통화 내용, 다음 담당자에게 전달할 내용을 기록합니다. + - 고객에게 보여도 되는 답변은 내부 메모가 아닌 댓글/답변에 작성합니다. + +5. **댓글/답변을 작성합니다.** + - Q&A 작성자가 읽는 안내입니다. + - 확인한 내용, 조치한 내용, 추가로 필요한 정보를 짧고 분명하게 씁니다. + - 처리 완료 안내가 필요한 경우 댓글 작성 화면의 `처리 완료 안내`를 선택합니다. + +6. **피드백 상태를 바꿉니다.** + - 답변이나 개발 진행 상황에 맞는 상태를 선택합니다. + - 상태만 바꾸고 답변을 남기지 않으면 작성자가 처리 결과를 알기 어렵습니다. + +### 피드백 상태 + +| 상태 | 언제 사용하나요? | +| --- | --- | +| 신규 | 새로 들어와 아직 확인하지 않은 요청입니다. | +| 접수 | 요청을 확인했고 처리 대상으로 접수한 상태입니다. | +| 진행중 | 답변 작성, 확인, 개발 작업 등 처리가 진행 중입니다. | +| 완료 | 안내와 필요한 처리가 끝난 상태입니다. | +| 보류 | 정보 부족, 일정, 외부 사유 등으로 잠시 멈춘 상태입니다. 보류 사유를 함께 남깁니다. | + +`진행중` 상태에서는 내부 처리 흐름에 따라 추가 안내가 표시될 수 있습니다. 화면에 보이는 상태와 고객에게 보내는 답변은 각각 확인합니다. + +## 7. 이슈 처리 방법 + +이슈 처리는 “피드백을 해결하기 위해 필요한 작업을 관리하는 일”입니다. + +### 이슈를 만들거나 연결할 때 + +| 상황 | 처리 방법 | +| --- | --- | +| 이미 같은 개발 작업이 있음 | 기존 이슈를 찾아 피드백에 연결합니다. | +| 새로운 개발 작업임 | 이슈를 새로 만들고 피드백을 연결합니다. | +| 단순 안내·문의임 | 이슈를 만들지 않고 답변 후 피드백을 처리합니다. | +| 같은 원인으로 문의가 여러 건임 | 하나의 이슈에 여러 피드백을 연결할 수 있습니다. | + +### 기본 처리 순서 + +```mermaid +flowchart TD + A[이슈 처리 탭에서 이슈 확인] --> B[프로젝트·채널·제목 확인] + B --> C[이슈 상세 열기] + C --> D[연결된 피드백 확인] + D --> E[이슈 담당자 지정] + E --> F{Gitea 개발 이슈가 있는가?} + F -- 있음 --> G[Gitea 상태와 작업 내용 확인] + F -- 없음 --> H[기존 Gitea 이슈 검색·연결 또는 새 이슈 생성] + H --> G + G --> I[내부 메모로 진행 내용 기록] + I --> J[이슈 상태 변경] + J --> K[연결된 피드백에 답변 및 상태 반영] +``` + +### 이슈 상세에서 확인할 것 + +- **이슈 내용**: 작업 제목과 상세 내용입니다. +- **이슈 상태**: 개발 작업이 어느 단계인지 표시합니다. +- **이슈 담당자**: 작업을 맡은 사람입니다. +- **연결된 피드백**: 같은 문제를 겪은 Q&A 목록입니다. 항목을 눌러 내용을 확인합니다. +- **담당자 내부 메모**: 개발·운영팀 간 진행 내용을 기록합니다. +- **Gitea 이슈 연결**: 번호나 제목으로 기존 개발 이슈를 검색해 연결합니다. + +### 이슈 상태 + +이슈도 피드백과 같은 상태 색상과 이름을 사용합니다. + +| 상태 | 의미 | +| --- | --- | +| 신규 | 작업이 등록되었지만 아직 본격적으로 시작하지 않았습니다. | +| 접수 | 작업 내용을 확인하고 처리 대상으로 받았습니다. | +| 진행중 | 개발·확인 작업을 하고 있습니다. | +| 완료 | 작업이 끝났습니다. 연결된 피드백의 답변·상태도 확인합니다. | +| 보류 | 작업을 잠시 멈춘 상태입니다. 이유를 내부 메모에 남깁니다. | + +이슈가 완료되어도 고객 피드백은 별도로 답변하고 상태를 바꿔야 합니다. 반대로 피드백에 답변했다고 이슈가 자동 완료되는 것도 아닙니다. + +## 8. 피드백과 이슈를 함께 처리하는 예시 + +### 예시: 로그인 오류 문의 + +| 순서 | 담당자가 하는 일 | +| --- | --- | +| 1 | `시스템 오류` 채널에서 로그인 오류 피드백을 확인합니다. | +| 2 | 담당자를 지정하고, 재현에 필요한 정보를 내부 메모에 남깁니다. | +| 3 | 개발 수정이 필요하면 기존 로그인 오류 이슈를 검색합니다. | +| 4 | 같은 이슈가 있으면 연결하고, 없으면 새 이슈를 생성합니다. | +| 5 | 피드백에는 “확인 중이며 수정 작업을 진행한다”는 답변을 작성합니다. | +| 6 | 피드백과 이슈를 각각 `진행중`으로 변경합니다. | +| 7 | 개발이 끝나면 이슈를 `완료`로 바꾸고, 피드백에 결과를 답변합니다. | +| 8 | 고객 안내까지 끝난 뒤 피드백을 `완료`로 변경합니다. | + +## 9. 댓글과 내부 메모 구분하기 + +| 작성 위치 | 누가 읽나요? | 작성할 내용 | +| --- | --- | --- | +| 댓글/답변 | Q&A 작성자와 운영팀 | 확인 결과, 처리 내용, 고객에게 전달할 안내 | +| 담당자 내부 메모 | 운영팀과 관리자 | 내부 협의, 원인 추정, 재현 방법, 인수인계 내용 | + +### 잘못 작성하기 쉬운 예 + +- 고객에게 보여주면 안 되는 개인정보·내부 협의 내용을 댓글에 작성하지 않습니다. +- 고객이 알아야 할 처리 결과를 내부 메모에만 남기지 않습니다. +- “확인 중”이라고만 쓰지 말고 무엇을 확인 중인지 함께 씁니다. + +## 10. 운영 전 확인 체크리스트 + +### 피드백을 열었을 때 + +- [ ] 프로젝트와 채널이 맞는가? +- [ ] 제목과 본문을 끝까지 확인했는가? +- [ ] 첨부파일이 있는가? +- [ ] 중요도를 확인했는가? +- [ ] 담당자가 지정되어 있는가? +- [ ] 기존 연결 이슈가 있는가? +- [ ] 답변과 내부 메모를 올바른 위치에 작성했는가? +- [ ] 피드백 상태를 실제 진행 상황과 맞게 바꿨는가? + +### 이슈를 처리했을 때 + +- [ ] 이슈가 어느 프로젝트·채널에 속하는지 확인했는가? +- [ ] 연결된 피드백을 모두 확인했는가? +- [ ] 기존 Gitea 이슈가 있는지 먼저 검색했는가? +- [ ] 이슈 담당자를 지정했는가? +- [ ] 진행 내용과 보류 사유를 내부 메모에 남겼는가? +- [ ] 이슈 상태와 피드백 상태를 각각 바꿨는가? +- [ ] 고객에게 보낼 답변을 남겼는가? + +## 11. 자주 생기는 상황 + +| 상황 | 먼저 확인할 것 | +| --- | --- | +| 목록에 피드백이 없다 | 프로젝트, 날짜, 상태 필터와 완료·보류 표시를 확인합니다. | +| 담당자가 보이지 않는다 | 해당 프로젝트의 담당 권한과 현재 선택한 프로젝트를 확인합니다. | +| 같은 문의가 반복된다 | 기존 이슈를 검색하고 여러 피드백을 하나의 이슈에 연결할 수 있는지 확인합니다. | +| 이슈는 완료됐는데 피드백이 남아 있다 | 이슈와 피드백은 별도 상태이므로 피드백 답변과 상태를 따로 처리합니다. | +| 고객에게 답변이 보이지 않는다 | 내부 메모가 아닌 댓글/답변으로 작성했는지 확인합니다. | +| 오래된 건을 찾고 싶다 | 날짜 검색 범위를 넓히고 `완료 상태 표시`·`보류 상태 표시`를 확인합니다. | +| 연결 항목 제목이 잘려 보인다 | 제목에 마우스를 올려 전체 내용을 확인하고, `외 N개`가 있는지 확인합니다. | + +## 12. 한 줄 요약 + +```text +프로젝트·채널 확인 + → 피드백 읽기 + → 담당자 지정 + → 내부 메모 또는 고객 답변 + → 필요하면 이슈 연결 + → 피드백 상태와 이슈 상태를 각각 업데이트 +``` + +이 순서만 지키면 Q&A 누락을 줄이고, 운영팀과 개발팀의 업무 경계를 명확하게 유지할 수 있습니다. diff --git a/docs/관리페이지 md 파일/제품-배포-및-EGBIM-데이터-이관-실행계획.md b/docs/관리페이지 md 파일/제품-배포-및-EGBIM-데이터-이관-실행계획.md new file mode 100644 index 0000000..c8372a3 --- /dev/null +++ b/docs/관리페이지 md 파일/제품-배포-및-EGBIM-데이터-이관-실행계획.md @@ -0,0 +1,889 @@ +# 제품 배포 및 EGBIM 데이터 이관 실행계획 + +## 1. 문서 목적 + +현재 상태를 다음과 같이 전제하고, 앞으로의 작업 순서와 의사결정 기준을 정리한다. + +- 스테이징 배포 완료 +- 스테이징 작성페이지에 EGBIM 홈페이지 UI를 적용하는 작업 진행 또는 검증 단계 +- 관리페이지와 API 연동 구조 존재 +- 제품 배포 파이프라인 설계 초안 작성 완료 +- 기존 EGBIM 홈페이지 데이터는 아직 제품 데이터로 최종 이관하지 않음 + +이 문서의 목표는 다음과 같다. + +1. 스테이징 기능을 제품 수준으로 안정화한다. +2. 같은 검증 결과를 사용해 product 환경으로 승격할 수 있는 배포 파이프라인을 완성한다. +3. 기존 EGBIM 데이터를 원본 보존·재실행·검증 가능한 방식으로 이관한다. +4. 내부 오픈 후 문제를 관찰하고, 외부 정식 오픈으로 안전하게 확장한다. + +--- + +## 2. 먼저 결정할 최적의 전체 순서 + +권장 순서는 다음과 같다. + +```text +현재 상태 + │ + ▼ +1. 기준선 고정 및 제품 요구사항 확정 + │ + ▼ +2. 스테이징 작성페이지·관리페이지·API 통합 검증 + │ + ▼ +3. 진입 루트 설계 및 스테이징 적용 + │ + ▼ +4. Product 배포 파이프라인 최소 운영 수준 구현 + │ + ▼ +5. Product 작성페이지·관리페이지·API 적용 + │ + ▼ +6. EGBIM 데이터 이관 설계 확정 및 도구 구현 + │ + ▼ +7. 스테이징 Dry Run 및 전체 이관 검증 + │ + ▼ +8. Product DB 백업 후 데이터 이관 + │ + ▼ +9. 내부 오픈 및 관찰 운영 + │ + ▼ +10. 외부 정식 오픈 +``` + +### 이 순서를 권장하는 이유 + +- 작성페이지와 관리페이지의 데이터 계약이 확정되기 전에 이관하면, 기존 데이터의 필드·상태·첨부파일 매핑을 다시 수정해야 한다. +- 진입 루트를 먼저 확정하면 SSO callback URL, API base URL, CORS, cookie domain, reverse proxy 규칙을 product 배포 전에 검증할 수 있다. +- Product 배포 파이프라인을 먼저 최소 수준으로 구현해야 스테이징에서 검증한 동일 버전을 product에 재현할 수 있다. +- 데이터 이관은 기존 데이터를 변경하거나 중복 생성할 위험이 있으므로, 내부 오픈 직전에 한 번에 시도하기보다 스테이징에서 반복 가능한 방식으로 검증해야 한다. +- 내부 오픈은 최종 사용자 전체 오픈이 아니라 업무 담당자 중심의 제한된 검증 구간으로 사용해야 한다. + +--- + +## 3. 단계별 실행 계획 + +### 0단계. 기준선 고정 및 오픈 범위 확정 + +가장 먼저 다음 내용을 문서로 확정한다. + +- 스테이징과 product의 도메인·포트·환경변수·Secret 분리 +- product에 적용할 commit SHA 또는 release tag +- product의 서비스 구성과 외부 연동 범위 +- EGBIM 기존 데이터의 이관 기준일 +- 내부 오픈 대상자와 권한 +- 외부 정식 오픈 대상 서비스 및 오픈 시각 +- 장애 발생 시 오픈 중단 기준 + +완료 조건: + +- product 오픈에 필요한 기능 목록과 제외 기능이 합의됨 +- 데이터 이관 기준일과 원본 데이터 보존 정책이 확정됨 +- 스테이징에서 검증할 시나리오 목록이 확정됨 + +### 1단계. 스테이징 작성페이지·관리페이지·API 통합 안정화 + +EGBIM 홈페이지 UI를 적용한 작성페이지를 실제 사용자 흐름 기준으로 검증한다. + +필수 검증 항목: + +- 작성페이지 진입 및 로그인/비로그인 정책 +- 필수값·선택값·HTML 본문·특수문자 처리 +- 비밀글 정책 +- 첨부파일 업로드·다운로드·실패 재시도 +- 작성 완료 후 관리페이지 목록 노출 +- 관리페이지 상세 조회 +- 상태·중요도·담당자 변경 +- 댓글과 내부 메모의 노출 범위 +- 동일 요청 재전송 시 중복 생성 방지 +- API 오류와 네트워크 오류 화면 처리 + +이 단계에서는 UI의 시각적 동일성만 확인하지 말고, EGBIM 화면이 보내는 데이터와 ABC API가 저장하는 데이터의 필드 매핑을 확정해야 한다. + +### 2단계. 진입 루트 설계 및 스테이징 적용 + +권장 진입 구조는 다음과 같다. + +```text +EGBIM 홈페이지 + ├─ Q&A 작성 → Product 작성페이지 + └─ 관리자 진입 → 통합 관리페이지 + +Product Web + ├─ 내부 API BFF + ├─ ABC API + └─ Secretary API +``` + +결정할 항목: + +- 기존 EGBIM URL에서 신규 작성페이지로 이동하는 방식 +- 기존 URL의 redirect 여부와 유지 기간 +- 관리자 URL과 일반 사용자 URL 분리 +- SSO callback 및 logout URL +- `/support`, `/admin`, `/api` 등 경로 규칙 +- reverse proxy에서 Web만 외부 공개할지 여부 +- 기존 검색엔진 링크와 즐겨찾기 대응 +- 잘못된 경로·권한 없는 경로·만료된 링크 처리 + +완료 조건: + +- 스테이징 도메인에서 실제 진입·로그인·작성·관리 흐름이 끝까지 동작함 +- callback URL, cookie, CORS, API route가 product 도메인 기준으로 변경 가능함 +- URL 전환 시 기존 사용자가 어디로 이동하는지 명확함 + +### 3단계. Product 배포 파이프라인 구현 + +파이프라인 설계 초안을 다음 최소 운영 수준까지 구현한다. + +```text +PR 검증 + ↓ +lint · format · typecheck · unit/integration/E2E + ↓ +commit SHA 이미지 Build + ↓ +Registry Push + ↓ +staging 배포 + ↓ +health check · smoke test + ↓ +승인 + ↓ +동일 이미지 digest로 product 배포 +``` + +현재 staging workflow는 소스를 SSH로 전송한 뒤 서버에서 다시 빌드한다. Product 배포에서는 다음을 우선 적용하는 것을 권장한다. + +- CI에서 API·Web·Secretary API 이미지를 동일 commit 기준으로 빌드 +- commit SHA 또는 release tag로 이미지 태깅 +- staging과 product에서 동일 이미지 digest 사용 +- 환경별 동시 배포 잠금 +- 배포 전 필수 환경변수와 외부 연결 Preflight +- 배포 전 MySQL 전체 백업과 checksum 생성 +- TypeORM과 Alembic Migration을 별도 one-shot job으로 직렬 실행 +- API → Secretary API → Web 순서로 기동 +- 배포 후 API·Redis·SSO·첨부파일·주요 업무 기능 Smoke Test +- commit SHA, 이미지 digest, migration revision, backup checksum 기록 + +완료 조건: + +- 실패한 CI 결과가 product 배포로 넘어가지 않음 +- product는 staging에서 검증한 동일 artifact를 사용함 +- 이전 이미지와 DB 백업 식별자로 롤백 절차를 재현할 수 있음 + +### 4단계. Product 작성페이지·관리페이지·API 적용 + +Product 환경에는 개발 브랜치의 최신 소스를 직접 배포하지 않고, staging 검증을 통과한 release artifact만 승격한다. + +적용 순서: + +1. Product Secret·Variable 등록 및 값 검증 +2. Product 도메인 기준 진입 루트·SSO callback 등록 +3. Product DB 연결 Preflight +4. 백업 생성 및 checksum 확인 +5. 필요한 Migration 실행 +6. API 및 Secretary API 기동 +7. 작성페이지와 관리페이지 기동 +8. 제한된 운영 계정으로 Smoke Test +9. 내부 오픈 승인 + +### 5단계. EGBIM 기존 데이터 이관 + +데이터 이관은 작성페이지와 API의 필드 계약이 확정된 뒤 진행한다. 이관 완료 전까지 기존 EGBIM 원본은 읽기 전용 또는 보존 상태로 유지한다. + +권장 방식은 다음 절의 “방안 A”이다. + +### 6단계. 내부 오픈 및 관찰 운영 + +내부 오픈은 다음 대상부터 시작한다. + +- 운영 담당자 +- 관리자 +- 개발·QA 담당자 +- 제한된 내부 사용자 + +최소 관찰 기간 동안 다음을 확인한다. + +- 작성 성공률 +- API 4xx/5xx 비율 +- 첨부파일 실패율 +- SSO 로그인 실패율 +- 관리자의 상태·담당자 변경 성공 여부 +- 알림 전송 성공 여부 +- 데이터 중복·누락 여부 +- 응답시간과 컨테이너 재시작 여부 + +### 7단계. 외부 정식 오픈 + +내부 오픈에서 치명적 장애가 없고, 데이터 이관 검증이 완료된 뒤 외부 정식 오픈을 진행한다. + +오픈 직전에는 다음을 다시 확인한다. + +- 기존 URL의 redirect 및 안내문 +- 외부 사용자의 작성 권한 +- 개인정보·비밀글 노출 정책 +- 첨부파일 저장소와 다운로드 권한 +- 장애 공지 및 문의 대응 담당자 +- 롤백 가능한 이전 이미지와 DB 백업 + +--- + +## 4. EGBIM 데이터 이관 권장 방안 + +### 방안 A. 읽기 전용 Export API + 중앙 Import Worker + +#### 권장도: 가장 높음 + +기존 EGBIM 홈페이지 서버에 일회성 읽기 전용 Export API를 만들고, 중앙 시스템의 Import Worker가 페이지 단위로 데이터를 읽어 Product API 또는 내부 import service로 저장한다. + +```text +기존 EGBIM + │ + │ 인증된 읽기 전용 Export API + ▼ +Import Worker + ├─ 필드·상태·사용자 매핑 + ├─ 첨부파일 다운로드 + ├─ checksum 검증 + └─ migration mapping 기록 + ▼ +ABC API / Product DB / R2 +``` + +#### 순차 실행 방법 + +##### A-1. 원본 데이터 목록과 범위 확정 + +- 게시글, 댓글, 첨부파일, 작성자, 상태, 작성일·수정일 목록화 +- 이관 기준일 확정 +- 삭제글·비밀글·관리자 메모의 이관 여부 결정 +- 원본의 전체 건수와 첨부파일 개수 기록 +- 원본 데이터의 시간대 확인 + +##### A-2. 매핑 규칙 확정 + +다음 매핑표를 먼저 확정하고 코드로 고정한다. + +| 원본 EGBIM | Product/ABC | 결정 사항 | +|---|---|---| +| 게시글 번호 | `source_post_id` 또는 migration metadata | 원본 추적용으로 보존 | +| 게시글 제목·본문 | feedback title/content | HTML·줄바꿈·특수문자 처리 | +| 작성자 | ABC 사용자 식별자 | SSO ID, 이메일, 전화번호 우선순위 | +| 원본 상태 | feedback status | 상태 변환표 필요 | +| 비밀글 | `is_secret` 등 | 사용자·관리자 노출 정책 확인 | +| 댓글 | ABC 댓글 | 작성자와 작성일 보존 | +| 첨부파일 | attachment metadata + R2/local object | 파일 checksum 보존 | +| 프로젝트/채널 | Product project/channel | 고정 UUID 또는 이름 매핑 | + +##### A-3. 원본 Export API 구축 + +예시: + +```http +GET /api/migration/qna?cursor=...&limit=100 +Authorization: Bearer +``` + +API는 다음 조건을 가져야 한다. + +- 읽기 전용 +- 일회성 토큰 또는 IP 제한 +- cursor 기반 페이지네이션 +- 게시글·댓글·첨부파일의 안정적인 원본 ID 제공 +- `created_at`, `updated_at` 제공 +- 재조회 가능한 정렬 기준 제공 +- 요청 로그와 반환 건수 기록 + +첨부파일은 원본 API에서 직접 다운로드하거나, 인증된 임시 URL을 발급하는 방식으로 분리한다. + +##### A-4. Import Worker 구현 + +Import Worker는 다음 정보를 모든 레코드에 남긴다. + +- `source_system = EGBIM_QA` +- `source_post_id` +- `source_comment_id` +- `migration_batch_id` +- 원본 checksum +- 중앙 저장 ID +- 처리 상태: `PENDING`, `MIGRATED`, `FAILED`, `SKIPPED` +- 실패 메시지 +- 최초 처리 시각과 최종 처리 시각 + +중복 방지를 위해 다음 키를 사용한다. + +```text +source_system + source_entity_type + source_entity_id +``` + +같은 batch를 다시 실행해도 기존 레코드를 새로 만들지 않고 기존 mapping을 조회하도록 한다. + +##### A-5. Staging Dry Run + +실제 저장 없이 다음만 수행한다. + +- 원본 건수 조회 +- 필드 매핑 가능 여부 검사 +- 사용자 식별 가능 여부 검사 +- 상태 변환 가능 여부 검사 +- 첨부파일 접근 가능 여부 검사 +- HTML·특수문자·긴 본문 검사 +- 누락·중복·미매핑 목록 생성 + +##### A-6. Staging 전체 이관 + +Dry Run 오류를 해결한 뒤 스테이징 DB에 전체 이관한다. + +검증 항목: + +- 원본 게시글 수 = Product 저장 게시글 수 +- 원본 댓글 수 = Product 저장 댓글 수 +- 원본 첨부파일 수 = Product 저장 첨부파일 metadata 수 +- 샘플 제목·본문·작성자·상태 비교 +- 원본 ID를 통해 Product 상세 페이지 추적 가능 +- 첨부파일 다운로드 가능 +- 같은 batch 재실행 시 중복이 생성되지 않음 + +##### A-7. Product 이관 전 백업 + +Product 이관 직전에 다음을 수행한다. + +- `userfeedback` 전체 백업 +- 백업 압축 및 암호화 +- SHA-256 checksum 생성 +- 첨부파일 저장소의 versioning 또는 별도 백업 확인 +- 백업 복원 테스트 또는 최소한 파일 무결성 테스트 +- 이관 시작 시각과 기준 commit 기록 + +##### A-8. Product 최종 이관 + +최종 이관은 다음 순서로 진행한다. + +1. 기존 EGBIM 원본을 읽기 전용으로 전환 +2. 이관 기준시각 기록 +3. 기준시각 이전 데이터를 전체 이관 +4. 이관 중 발생한 오류를 별도 실패 목록으로 분리 +5. 건수·checksum·샘플 검증 +6. 기준시각 이후 변경분이 있다면 증분 이관 +7. 관리 담당자 승인 +8. 신규 작성 경로를 Product로 전환 +9. 기존 EGBIM에 안내문 또는 redirect 적용 + +#### 장점 + +- 원본 DB 스키마에 강하게 결합되지 않는다. +- 필요한 데이터만 Product 모델에 맞게 변환할 수 있다. +- 원본 ID와 중앙 ID의 mapping을 보존할 수 있다. +- 실패한 일부 batch만 재처리할 수 있다. +- 재실행 시 중복 방지가 쉽다. + +#### 단점 + +- 원본 Export API 개발이 필요하다. +- 필드·상태·사용자·첨부파일 매핑 작업이 필요하다. +- 대량 첨부파일 이관 시간이 오래 걸릴 수 있다. + +--- + +## 5. EGBIM 데이터 이관 대안 + +### 방안 B. 기존 DB Dump 후 ETL 변환 + +#### 방식 + +기존 EGBIM DB를 SQL dump로 백업한 뒤, 별도의 임시 MySQL에 복원한다. ETL 스크립트가 임시 DB에서 데이터를 읽어 Product의 ABC API 또는 import 테이블로 전달한다. + +```text +기존 EGBIM DB + ↓ SQL dump +임시 변환 DB + ↓ ETL +Product import API / ABC DB +``` + +#### 순서 + +1. 원본 DB 전체 dump +2. dump checksum 생성 +3. 임시 DB 복원 +4. 원본 테이블 구조와 데이터 건수 확인 +5. 변환 스크립트 실행 +6. Product staging에 적재 +7. 검증 및 보정 +8. Product 백업 후 최종 적재 + +#### 장점 + +- 원본 Export API를 새로 만들 필요가 없다. +- 대량 데이터 추출이 빠르다. +- 원본 테이블을 직접 조회하므로 누락을 확인하기 쉽다. + +#### 단점 + +- 기존 DB 스키마에 강하게 결합된다. +- 개인정보와 원본 전체 DB가 변환 서버에 복사된다. +- Product 스키마와 원본 스키마가 다르면 변환 로직이 복잡해진다. +- 운영 DB에 직접 SQL을 실행할 위험이 있다. +- 재실행·중복 방지·첨부파일 매핑을 별도로 구현해야 한다. + +#### 적용 조건 + +- 원본 DB 접근 권한을 안전하게 확보할 수 있을 때 +- Export API를 추가할 수 없을 때 +- 원본 DB의 schema와 첨부파일 저장 위치를 정확히 파악했을 때 + +단순히 기존 SQL dump를 Product DB에 그대로 복원하는 방식은 권장하지 않는다. Product의 ABC·Secretary 스키마와 원본 EGBIM 스키마가 다르고, 데이터 소유권과 ID 체계도 다르기 때문이다. + +### 방안 C. 파일 기반 Export(JSON/CSV + 첨부파일 묶음) + +#### 방식 + +기존 EGBIM에서 게시글·댓글·사용자·첨부파일 metadata를 JSON 또는 CSV로 추출하고, 첨부파일은 별도 archive로 묶어 Import Worker에 전달한다. + +```text +qna.json +comments.json +users.json +attachments-manifest.json +attachments.tar.gz +``` + +#### 순서 + +1. 추출 스크립트 작성 +2. 파일별 schema와 encoding 확정 +3. 각 파일 checksum 생성 +4. 첨부파일 manifest 생성 +5. staging Import Worker 실행 +6. 검증 리포트 생성 +7. Product 백업 후 최종 import + +#### 장점 + +- 원본 시스템에 API를 추가하지 않아도 된다. +- 결과물을 보관하고 재검증하기 쉽다. +- 소규모·일회성 이관에 적합하다. +- 운영 DB에 지속적으로 접근하지 않아도 된다. + +#### 단점 + +- 추출 시점 이후 변경분을 자동으로 반영하기 어렵다. +- 대용량 파일 전달·보관·암호화 관리가 필요하다. +- JSON/CSV 포맷이 원본 데이터의 모든 관계를 표현하지 못할 수 있다. +- 파일 재생성 시 원본과 결과의 동일성을 추적해야 한다. + +#### 적용 조건 + +- 원본 데이터를 특정 시점에 동결할 수 있을 때 +- 데이터 규모가 관리 가능한 수준일 때 +- Export API를 운영할 수 없고, DB 직접 접근도 제한될 때 + +--- + +## 6. 데이터 이관 방안 비교 및 최종 결정 + +| 기준 | 방안 A: Export API + Worker | 방안 B: DB Dump + ETL | 방안 C: JSON/CSV 파일 Export | +|---|---:|---:|---:| +| 원본 스키마 결합도 | 낮음 | 높음 | 중간 | +| 대량 추출 성능 | 중간 | 높음 | 중간~높음 | +| 재실행·증분 이관 | 매우 좋음 | 구현 필요 | 제한적 | +| 개인정보 노출 범위 | 비교적 작음 | 큼 | 중간 | +| 원본 변경 추적 | 좋음 | 좋음 | 낮음 | +| 첨부파일 처리 | 좋음 | 별도 구현 | 별도 archive 필요 | +| 초기 개발 난이도 | 중간 | 높음 | 낮음~중간 | +| 운영 안전성 | 가장 좋음 | 주의 필요 | 보통 | + +### 최종 권장안 + +방안 A를 기본으로 선택한다. + +구체적으로는 다음 조합을 권장한다. + +```text +원본: 읽기 전용 Export API +추출: cursor 기반 batch +변환: 별도 Import Worker +저장: Product 내부 import service 또는 ABC API +추적: migration_batches + migration_mappings +첨부파일: manifest·checksum 검증 후 R2 또는 승인된 저장소 업로드 +재실행: source_system/source_entity_id 기준 idempotent 처리 +``` + +단, 원본 EGBIM 서버에 Export API를 추가할 수 없는 경우에만 방안 C를 우선 검토하고, 원본 DB 접근이 불가피한 경우에 방안 B를 사용한다. + +--- + +## 7. 이관 시 반드시 지켜야 할 원칙 + +### 원본 보존 + +- 원본 EGBIM DB와 첨부파일을 이관 완료 후 즉시 삭제하지 않는다. +- 최소한 이관 완료 및 외부 오픈 후 안정화 기간까지 보존한다. +- 원본 dump 또는 export 파일은 암호화하고 접근 권한을 제한한다. + +### 멱등성 + +- 같은 데이터를 다시 실행해도 중복 생성되지 않아야 한다. +- 게시글, 댓글, 첨부파일 각각의 원본 식별자를 보존한다. +- 실패한 레코드만 재처리할 수 있어야 한다. + +### 기준시각과 증분 이관 + +- 전체 이관 시작 전에 기준시각을 기록한다. +- 이관 중 원본이 계속 변경되면 기준시각 이후 변경분을 별도로 추출한다. +- 최종 전환 직전에 증분 이관을 한 번 더 실행한다. + +### 첨부파일 무결성 + +- 파일명만 비교하지 않고 크기와 SHA-256 checksum을 비교한다. +- DB metadata와 실제 객체가 모두 존재하는지 확인한다. +- Product에서 다운로드 권한과 비밀글 접근 권한을 검증한다. + +### 사용자 식별 + +- 이메일만으로 사용자를 무조건 병합하지 않는다. +- SSO 식별자, 기존 사용자 ID, 이메일, 전화번호 순으로 매칭 정책을 정한다. +- 매칭되지 않는 사용자는 임의 병합하지 말고 관리자 검토 목록으로 보낸다. + +--- + +## 8. 단계별 Go/No-Go 기준 + +### Product 배포 전 + +- [ ] CI 전체 성공 +- [ ] staging smoke test 성공 +- [ ] product용 Secret·Variable 검증 +- [ ] 이미지 commit SHA·digest 확정 +- [ ] Migration 순서와 호환성 확인 +- [ ] DB 백업 및 checksum 성공 +- [ ] 롤백 이미지와 복원 백업 식별 + +### 데이터 이관 전 + +- [ ] 원본 전체 건수 확인 +- [ ] 필드·상태·사용자·첨부파일 매핑 확정 +- [ ] migration batch 식별자 발급 +- [ ] staging dry run 성공 +- [ ] staging 전체 이관 성공 +- [ ] 재실행 시 중복 없음 확인 +- [ ] 첨부파일 checksum 검증 성공 + +### 내부 오픈 전 + +- [ ] Product 최종 이관 성공 +- [ ] 이관 전후 건수 비교 완료 +- [ ] 샘플 상세·댓글·첨부파일 확인 +- [ ] 관리자 로그인 및 권한 확인 +- [ ] 기존 EGBIM 링크의 이동 정책 적용 +- [ ] 로그·모니터링·장애 대응 담당자 확정 + +### 외부 정식 오픈 전 + +- [ ] 내부 오픈 관찰 기간 종료 +- [ ] 치명적 오류와 데이터 누락 없음 +- [ ] 작성·조회·관리·첨부파일 주요 흐름 정상 +- [ ] 고객 안내 및 문의 대응 준비 +- [ ] 기존 시스템 read-only/redirect 정책 확정 +- [ ] 오픈 당일 담당자와 중단 기준 확보 + +--- + +## 9. 최종 실행 순서 요약 + +```text +1. 제품 범위·데이터 기준일·오픈 정책 확정 +2. 스테이징 작성페이지와 관리페이지의 전체 업무 흐름 검증 +3. 진입 루트·SSO callback·redirect·reverse proxy 적용 +4. CI 결과가 배포를 통제하도록 Product pipeline 보완 +5. staging 검증 artifact로 Product 작성/관리 기능 배포 +6. EGBIM Export API와 Import Worker 구현 +7. staging Dry Run → 전체 이관 → 재실행 검증 +8. Product DB·첨부파일 백업 및 checksum 확인 +9. EGBIM 원본 read-only 전환 +10. Product 최종 전체 이관 +11. 기준시각 이후 변경분 증분 이관 +12. 내부 오픈 및 모니터링 +13. 안정화 확인 후 외부 정식 오픈 +``` + +최종적으로는 “기존 DB를 Product DB에 그대로 복원”하는 방식보다, 원본을 읽기 전용으로 유지하면서 Export API와 Import Worker를 통해 Product의 정식 데이터 모델로 변환하는 방식을 채택하는 것이 가장 안전하다. + +--- + +## 10. 운영 중인 EGBIM 홈페이지의 데이터 이관 전략 + +### 10.1 현재 상황에 대한 전제 + +`eg-bim.com`은 현재 사용자가 계속 Q&A를 작성하는 운영 서비스이고, Q&A 플랫폼이 정식 운영되면 기존 홈페이지는 폐쇄할 예정이다. + +따라서 이관의 핵심 문제는 단순한 Dump 방식 선택이 아니다. + +```text +이관 시작 후에도 원본에 신규 글·수정·댓글·첨부파일이 계속 발생함 + ↓ +이관 데이터와 실제 원본 데이터 사이에 시간 차이가 발생함 + ↓ +최종 전환 시점의 누락·중복·충돌을 처리해야 함 +``` + +이번 프로젝트는 기존 홈페이지를 장기간 공존시키는 사업이 아니라, 기존 운영 시스템을 Product로 교체하는 사업이다. 그러므로 복잡한 실시간 양방향 연동을 영구적으로 구축하기보다, **전환 시점의 데이터 정합성을 확보하는 일회성 이관**에 집중하는 것이 적절하다. + +### 10.2 최종 권장안 + +다음 방식을 권장한다. + +```text +1. 평상시: 기존 EGBIM은 계속 운영 +2. 사전 준비: 반복 가능한 이관 도구와 staging 검증 완료 +3. 전환 직전: 기존 EGBIM의 신규 Q&A 작성 일시 중지 +4. 원본을 read-only로 전환 +5. 최종 Dump/Export 및 첨부파일 백업 +6. Product로 변환·적재 +7. 건수·본문·댓글·첨부파일 검증 +8. Product 작성페이지를 오픈 +9. 기존 EGBIM은 일정 기간 read-only 또는 redirect로 유지 +10. 안정화 후 기존 홈페이지 폐쇄 +``` + +핵심은 “운영 중에 계속 실시간 복제하다가 어느 순간 끊는 방식”이 아니라, **이관 준비는 운영 중에 수행하고 실제 최종 데이터 확정만 짧은 점검 시간에 수행하는 방식**이다. + +### 10.3 권장 전환 절차 + +#### T-14일 ~ T-7일: 사전 이관 준비 + +- Product의 프로젝트·채널·상태·사용자 매핑 확정 +- 기존 EGBIM 데이터 전체 목록과 첨부파일 목록 생성 +- Export API 또는 파일 Export 도구 구축 +- Import Worker와 mapping 테이블 구축 +- staging에 과거 전체 데이터 이관 +- staging에서 재실행해도 중복이 생기지 않는지 검증 +- 원본과 Product의 건수·샘플·첨부파일 checksum 비교 +- Product 배포 및 롤백 절차 검증 + +이 단계에서는 기존 EGBIM의 신규 작성 기능을 막지 않는다. 운영 데이터에 영향을 주지 않는 읽기 전용 추출만 수행한다. + +#### T-3일 ~ T-1일: 최종 전환 리허설 + +- 실제 운영 데이터와 동일한 규모로 이관 시간 측정 +- Dump/Export부터 Product 적재까지 걸리는 시간 측정 +- 실패 batch 재처리 시간 측정 +- 첨부파일 업로드 시간과 저장소 용량 확인 +- 최종 점검 시간에 처리 가능한지 확인 +- 오픈 당일 담당자와 의사결정권자 확정 + +최종 이관 시간이 허용 가능한 점검 시간보다 길다면, 그때만 증분 동기화 또는 CDC 도입을 검토한다. + +#### T-0: 작성 중지 및 최종 이관 + +1. 기존 EGBIM에 점검 공지 노출 +2. 신규 Q&A 작성·수정·댓글·첨부파일 등록 중지 +3. 기존 글 조회는 허용하거나 점검 안내 화면으로 전환 +4. 관리자도 데이터 수정 작업을 중지 +5. 원본 DB와 첨부파일 전체 백업 +6. 백업 파일 checksum 기록 +7. 최종 Export/Dump 실행 +8. Product import 실행 +9. 원본과 Product의 건수 및 샘플 비교 +10. 실패 레코드가 없거나 승인된 예외 목록에만 존재하는지 확인 +11. Product 작성페이지와 관리페이지 Smoke Test +12. Product의 신규 작성 기능 활성화 +13. 기존 EGBIM을 read-only 또는 redirect로 전환 + +신규 작성 중지는 가능한 한 짧게 유지하되, 이관 결과 검증이 끝나기 전에는 기존 시스템과 Product 양쪽에서 동시에 신규 작성을 허용하지 않는다. + +#### T+1일 ~ 안정화 기간: 기존 홈페이지 보존 + +기존 홈페이지를 즉시 삭제하지 않는다. + +- 기존 도메인은 Product 안내 또는 redirect로 유지 +- 원본 DB와 첨부파일은 보존 +- 관리자용 read-only 조회 경로 유지 +- 이관 누락·오류 문의에 대응 +- 안정화 기간 종료 후 별도 승인으로 폐쇄 + +### 10.4 운영 중 연동 방식별 선택지 + +#### 방안 A. 작성 중지 후 최종 Dump/Export + +##### 권장도: 가장 높음 + +운영 중에는 사전 추출과 staging 검증만 수행하고, 최종 전환 시점에만 작성 기능을 잠시 막은 뒤 최종 데이터를 이관한다. + +```text +운영 중 + └─ 사전 추출·리허설·검증 + +전환 시점 + └─ 작성 중지 → 최종 추출 → Product 적재 → 검증 → 오픈 +``` + +장점: + +- 데이터 기준시점이 명확하다. +- 신규 글 누락과 동시 수정 충돌을 방지할 수 있다. +- 실시간 동기화 시스템을 새로 운영하지 않아도 된다. +- 기존 홈페이지를 폐쇄할 계획과 가장 잘 맞는다. +- 장애 발생 시 기존 홈페이지를 다시 read-only 기준으로 사용할 수 있다. + +단점: + +- 최종 이관 시간 동안 작성 기능을 중지해야 한다. +- 최종 Dump/Import가 점검 시간 안에 끝나야 한다. +- 이관 중 Product 오픈이 지연되면 작성 중지 시간이 늘어날 수 있다. + +이 방식은 “기존 홈페이지 폐쇄 전환”이라는 현재 상황에 가장 적합하다. + +#### 방안 B. 초기 이관 + 실시간 또는 준실시간 API 증분 연동 + +##### 권장도: 조건부 + +기존 EGBIM을 계속 운영하면서 다음 조건의 데이터를 주기적으로 Product로 보낸다. + +```text +1. 전체 초기 이관 +2. updated_at 또는 cursor 기준 증분 조회 +3. 신규 글·수정 글·댓글·첨부파일 전송 +4. 최종 전환 시점에 마지막 증분 동기화 +5. 기존 EGBIM 작성 중지 +6. Product를 최종 오픈 +``` + +필요 조건: + +- 원본에 안정적인 `id`, `created_at`, `updated_at`이 있어야 함 +- 삭제 또는 비공개 변경을 알 수 있는 tombstone 또는 변경 이력 필요 +- 댓글·첨부파일도 증분 조회 가능해야 함 +- 원본과 Product의 mapping 및 idempotency key 필요 +- API 재시도와 순서 뒤바뀜을 처리해야 함 +- 마지막 증분 처리 완료 여부를 확인해야 함 + +장점: + +- 작성 중지 시간을 짧게 줄일 수 있다. +- 최종 이관량이 작아진다. +- Product 전환 직전까지 데이터를 따라갈 수 있다. + +단점: + +- 단순 API 호출이 아니라 동기화 시스템이 필요하다. +- 수정·삭제·비밀글 변경·첨부파일 변경을 모두 처리해야 한다. +- 원본과 Product의 상태가 잠시 불일치할 수 있다. +- 동기화 오류를 감시하고 재처리해야 한다. +- 기존 홈페이지를 폐쇄하는 일회성 프로젝트에 비해 개발·운영 비용이 크다. + +이 방식은 최종 Dump/Import 시간이 너무 길거나, 작성 중지를 거의 허용할 수 없을 때 선택한다. 단순히 “글 목록을 주기적으로 호출”하는 수준은 운영 이관 방식으로 충분하지 않다. + +#### 방안 C. 원본과 Product의 Dual Write + +##### 권장도: 낮음 + +기존 EGBIM에 글이 작성될 때 기존 DB와 Product API에 동시에 저장한다. + +장점: + +- 새로 작성되는 데이터를 거의 실시간으로 반영할 수 있다. +- 최종 전환 시 신규 데이터량이 적다. + +단점: + +- 두 시스템 중 한 곳만 성공하는 부분 실패가 발생할 수 있다. +- 재시도·보상 처리·순서 보장·중복 방지가 필요하다. +- 기존 홈페이지 코드를 크게 수정해야 한다. +- Product API 장애가 기존 EGBIM 작성 장애로 전파될 수 있다. +- 기존 시스템을 폐쇄할 예정이므로 투자 대비 활용 기간이 짧다. + +따라서 이번 프로젝트에서는 사용하지 않는 것을 권장한다. Dual Write가 꼭 필요하다면 직접 API를 두 번 호출하지 말고, 원본에 Outbox 이벤트를 기록한 뒤 비동기 Worker가 Product로 전달하는 구조를 사용해야 한다. + +### 10.5 Dump와 실시간 연동의 최종 판단 기준 + +다음 기준으로 결정한다. + +| 판단 기준 | Dump + 작성 중지 | 증분 API 연동 | +|---|---:|---:| +| 최종 이관 시간이 짧음 | 적합 | 과도함 | +| 몇 시간의 작성 중지가 가능함 | 적합 | 필요 없음 | +| 원본 폐쇄 예정 | 매우 적합 | 장기 운영 비용 과다 | +| 작성 중지를 거의 허용할 수 없음 | 부적합 | 적합 | +| 삭제·수정 이력 제공이 불완전함 | 적합 | 위험 | +| 첨부파일이 많고 처리 시간이 김 | 사전 리허설 필요 | 조건부 적합 | +| 원본 API 개발이 어려움 | 파일/DB Export로 가능 | 부적합 | + +현재 주어진 조건에서는 다음과 같이 결정한다. + +> **방안 A를 기본으로 채택한다.** +> 운영 중에는 이관 도구를 만들고 staging에서 반복 검증한다. Product 정식 전환 시점에는 EGBIM의 작성 기능을 일시 중지하고, 최종 Dump/Export 후 Product 검증을 완료한 뒤 신규 작성을 Product로 전환한다. + +### 10.6 최종 이관을 위한 최소 데이터 모델 + +최종 이관은 단순히 게시글을 INSERT하는 작업으로 만들지 않는다. 최소한 다음 추적 정보를 남긴다. + +```text +migration_batch + ├─ batch_id + ├─ source_system + ├─ snapshot_started_at + ├─ snapshot_completed_at + ├─ source_cutoff_at + ├─ source_total_count + ├─ imported_count + ├─ failed_count + └─ checksum + +migration_mapping + ├─ batch_id + ├─ source_entity_type + ├─ source_entity_id + ├─ target_entity_id + ├─ source_checksum + ├─ status + ├─ error_message + └─ processed_at +``` + +이 구조가 있어야 다음이 가능하다. + +- 어떤 원본 글이 Product의 어느 ID가 되었는지 추적 +- 실패한 글만 재처리 +- 같은 Dump를 다시 실행해도 중복 방지 +- 이관 결과를 원본 건수와 비교 +- 폐쇄 후에도 원본과 Product의 연결 관계 확인 + +### 10.7 권장 운영 정책 + +- 최종 이관일에는 기존 EGBIM에 신규 작성 중지 안내를 사전에 공지한다. +- 작성 중지와 동시에 신규 작성뿐 아니라 댓글·첨부파일 등록도 중지한다. +- 최종 이관 중 기존 데이터를 수정할 수 없게 한다. +- Product 검증 완료 전에는 기존 EGBIM을 삭제하지 않는다. +- Product 오픈 후에도 기존 도메인과 원본 백업을 보존한다. +- 기존 홈페이지 폐쇄는 Product 오픈과 별도의 승인 작업으로 처리한다. +- 이관 실패 시 원본을 다시 쓰기 가능 상태로 복구할 수 있어야 한다. + +### 10.8 업데이트된 최종 실행 순서 + +```text +1. Product 기능·API·진입 루트 확정 +2. 이관 도구 구현 +3. 운영 중 원본에서 staging으로 반복 Dry Run +4. 실제 데이터 규모 기준 이관 시간 측정 +5. Product 배포 pipeline과 rollback 검증 +6. 최종 전환 일정 공지 +7. EGBIM 작성·수정·댓글·첨부파일 등록 중지 +8. 원본 read-only 전환 및 최종 Dump/Export +9. Product DB·첨부파일 백업 확인 +10. Product import 실행 +11. 건수·checksum·샘플·첨부파일 검증 +12. Product 신규 작성 활성화 +13. 기존 EGBIM read-only/redirect 유지 +14. 내부 오픈 및 안정화 관찰 +15. 외부 정식 오픈 +16. 안정화 기간 종료 후 기존 EGBIM 폐쇄 +``` diff --git a/package.json b/package.json index 18a8fea..b75a42c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "baron-qa-gateway-test", "private": true, + "type": "module", "scripts": { "deploy": "wrangler deploy", "r2:upload": "bash scripts/upload-r2.sh", diff --git a/qa-secrets.json.example b/qa-secrets.json.example index 324eab4..760dafb 100644 --- a/qa-secrets.json.example +++ b/qa-secrets.json.example @@ -1,3 +1,4 @@ { - "SESSION_SECRET": "replace-with-openssl-rand-hex-32-output" + "SESSION_SECRET": "replace-with-openssl-rand-hex-32-output", + "ABC_API_KEY": "replace-with-project-scoped-abc-api-key" } diff --git a/src/index.js b/src/index.js index ae23e0a..9fb0863 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ export default { if (url.pathname === '/auth/callback') return finishLogin(request, env); if (url.pathname === '/auth/logout') return logout(request, env); if (url.pathname === '/auth/session') return sessionResponse(request, env); + if (url.pathname === '/api/feedbacks') return createFeedback(request, env); if (request.method !== 'GET' && request.method !== 'HEAD') { return json({ error: 'method_not_allowed' }, 405); @@ -141,6 +142,21 @@ function randomString(size = 32) { return base64url(randomBytes(size)); } +function uuidv7() { + const bytes = randomBytes(16); + const timestamp = Date.now(); + bytes[0] = Math.floor(timestamp / 0x10000000000) & 0xff; + bytes[1] = Math.floor(timestamp / 0x100000000) & 0xff; + bytes[2] = Math.floor(timestamp / 0x1000000) & 0xff; + bytes[3] = Math.floor(timestamp / 0x10000) & 0xff; + bytes[4] = Math.floor(timestamp / 0x100) & 0xff; + bytes[5] = timestamp & 0xff; + bytes[6] = (bytes[6] & 0x0f) | 0x70; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + function getCookie(request, name) { const header = request.headers.get('cookie') || ''; const match = header.split(';').map((part) => part.trim()).find((part) => part.startsWith(name + '=')); @@ -166,6 +182,8 @@ function requiredAuthConfig(env) { function normalizeClaims(claims) { const profile = claims.profile && typeof claims.profile === 'object' ? claims.profile : {}; const custom = claims.customAttributes || claims.custom_attributes || {}; + const profilePhones = Array.isArray(profile.phones) ? profile.phones : []; + const profilePhone = profile.phone || profile.phone_number || profilePhones[0]?.number || profilePhones[0]?.value || profilePhones[0] || ''; const tenants = claims.tenants || claims.tenantIds || custom.tenants || []; const tenantList = Array.isArray(tenants) ? tenants : Object.keys(tenants).map((key) => ({ id: key, ...(tenants[key] || {}) })); const tenantId = claims.tenant_id || claims.tenantId || custom.tenant_id || custom.tenantId || tenantList[0]?.id || tenantList[0]?.tenantId || ''; @@ -184,7 +202,7 @@ function normalizeClaims(claims) { loginId: claims.email || claims.loginId || claims.preferred_username || profile.email || '', email: claims.email || profile.email || '', name: claims.name || profile.name || claims.display_name || '', - phone: claims.phone || claims.phone_number || '', + phone: claims.phone || claims.phone_number || profilePhone, company: custom.company || claims.company || claims.organization || '', familyCompany: custom.familyCompany || custom.family_company || claims.familyCompany || '', department: custom.team || custom.department || claims.department || '' @@ -267,6 +285,85 @@ async function sessionResponse(request, env) { return json({ authenticated: Boolean(user), user }, 200, { 'access-control-allow-credentials': 'true' }); } +async function createFeedback(request, env) { + if (request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: 'POST' }); + const user = await getSession(request, env); + if (!user) return json({ error: 'unauthenticated' }, 401); + if (!env.ABC_API_KEY) return json({ error: 'feedback_api_not_configured' }, 503); + if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: 'feedback_target_not_configured' }, 503); + + let input; + try { + input = await request.json(); + } catch (error) { + return json({ error: 'invalid_json' }, 400); + } + if (!input || typeof input !== 'object' || Array.isArray(input)) return json({ error: 'invalid_json' }, 400); + + const title = String(input.title || '').trim(); + const contents = String(input.contents || '').trim(); + const category = String(input.Category || '').trim(); + if (!title || !contents || !category) return json({ error: 'title_contents_category_required' }, 400); + if (!['ERROR_QNA', 'IMPROVEMENT_QNA', 'GENERAL_QNA'].includes(category)) return json({ error: 'invalid_category' }, 400); + + const feedbackId = isUuidv7(input.feedbackId) ? input.feedbackId : uuidv7(); + const requesterId = user.requesterId || user.ssoSubject || user.userUuid; + const requesterTenantId = user.requesterTenantId || user.tenantId; + if (!requesterId || !requesterTenantId) return json({ error: 'requester_identity_missing' }, 422); + if (!user.phone) return json({ error: 'requester_phone_missing' }, 422); + + const upstreamPayload = { + title, + contents, + Category: category, + requester_id: requesterId, + requester_tenant_id: requesterTenantId, + requester_email: user.email || '', + requester_name: user.name || '', + requester_department: user.department || '', + requester_phone_number: user.phone, + is_secret: input.is_secret ? 1 : 0 + }; + if (Array.isArray(input.images) && input.images.length) upstreamPayload.images = input.images; + + const apiBaseUrl = String(env.ABC_API_BASE_URL || 'https://feedback.hmac.kr').replace(/\/$/, ''); + const endpoint = `${apiBaseUrl}/api/projects/${encodeURIComponent(env.ABC_PROJECT_ID)}/channels/${encodeURIComponent(env.ABC_CHANNEL_ID)}/feedbacks`; + const sourceNamespace = env.ABC_SOURCE_NAMESPACE || 'EGBIM_QA'; + const idempotencyConsumer = env.ABC_IDEMPOTENCY_CONSUMER || sourceNamespace; + let response; + try { + response = await fetch(endpoint, { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + 'x-api-key': env.ABC_API_KEY, + 'x-source-namespace': sourceNamespace, + 'x-source-record-id': feedbackId, + 'x-idempotency-consumer': idempotencyConsumer, + 'idempotency-key': feedbackId + }, + body: JSON.stringify(upstreamPayload) + }); + } catch (error) { + console.error('feedback_api_request_failed', error instanceof Error ? error.message : 'unknown'); + return json({ error: 'feedback_api_unreachable' }, 502); + } + + const result = await response.json().catch(() => ({})); + if (!response.ok) { + console.error('feedback_api_rejected', response.status); + return json({ error: 'feedback_api_rejected', status: response.status }, response.status >= 500 ? 502 : response.status); + } + const id = result.id || result.data?.id || result.feedback?.id; + if (!id) return json({ error: 'feedback_id_missing' }, 502); + return json({ id }); +} + +function isUuidv7(value) { + return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); +} + function logout(request, env) { const url = new URL(request.url); const target = safeReturnUrl(env.AUTH_POST_LOGOUT_REDIRECT_URI || '/', url.origin); diff --git a/wrangler.toml b/wrangler.toml index 7cf48b0..10c42db 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -19,6 +19,11 @@ AUTH_USERINFO_URL = "https://app.brsw.kr/oidc/userinfo" SESSION_COOKIE_NAME = "baron_qa_session" OAUTH_COOKIE_NAME = "baron_qa_oauth" SESSION_TTL_SECONDS = "3600" +ABC_API_BASE_URL = "https://feedback.hmac.kr" +ABC_PROJECT_ID = "01a0ae3f-fcf6-74b5-bdc4-70d942d6ad72" +ABC_CHANNEL_ID = "01a0ae40-51c2-7647-a93c-0249b3759777" +ABC_SOURCE_NAMESPACE = "EGBIM_QA" +ABC_IDEMPOTENCY_CONSUMER = "EGBIM_QA" PUBLIC_EXACT_PATHS = "/,/index.html,/favicon.ico" PUBLIC_PREFIXES = "/egbim/,/tova/,/gaia/,/shared/,/assets/,/auth/"