콘텐츠로 이동

관리자 콘솔

ADMIN 권한을 가진 계정이 회원·결제·쿠폰·리포트·피드백·ES 인덱스·감사 로그를 관리하는 백오피스. 프론트엔드는 /admin 라우트의 단일 SPA로 묶여 있고, 백엔드는 /admin/** 경로 prefix + Spring Security가 ADMIN 권한을 강제한다.

사용자 여정

어드민 회원 권한·상태 변경

sequenceDiagram
    autonumber
    participant U as 어드민
    participant FE as 프론트엔드 (/admin)
    participant API as Backend (AdminController)
    participant SVC as AdminService
    participant DB as MySQL

    U->>FE: "권한 관리" 탭 → 계정 검색
    FE->>API: GET /admin/accounts/roles?searchWord=...
    API->>SVC: getAllAccountRoles(pageable, searchWord)
    SVC->>DB: Account 페이지 조회 (이름/폰 LIKE)
    SVC-->>FE: { content: [...], pageable }

    U->>FE: 특정 계정 → 권한 변경 (USER → ADMIN)
    FE->>API: PATCH /admin/accounts/{accountId}/role
{ role: ADMIN } API->>SVC: changeAccountRole(accountId, role) SVC->>DB: Account.role = ADMIN SVC-->>FE: 200 OK

무통장 입금 수동 승인

sequenceDiagram
    autonumber
    participant U as 어드민
    participant FE as 프론트엔드 (/admin)
    participant API as Backend (AdminController)
    participant SVC as AdminService
    participant DB as MySQL

    U->>FE: "주문 관리" 탭 → 미승인 결제 필터
    FE->>API: GET /admin/payments?status=PRE_VERIFICATION
    API-->>FE: 무통장 입금 대기 목록

    U->>FE: 입금 확인 → 시작일 입력
    FE->>API: PATCH /admin/payments/{paymentId}/bank-confirm
{ startDate } API->>SVC: confirmBankPayment(paymentId, startDate) SVC->>DB: Payment.status = CONFIRMED
Membership 활성화 (start/end date) SVC-->>FE: 200 OK

ES 문서 편집 + 감사 로그

sequenceDiagram
    autonumber
    participant U as 어드민
    participant FE as 프론트엔드 (/admin)
    participant API as Backend (AdminElasticController)
    participant SVC as AdminElasticService
    participant AUD as ElasticsearchAuditService
    participant ES as Elasticsearch
    participant DB as MySQL

    U->>FE: "참조 데이터" 탭 → 인덱스 선택 → 문서 수정
    FE->>API: PUT /admin/elastic/{indexName}/documents/{documentId}
    API->>SVC: updateDocument(index, id, request, adminAccountId)
    SVC->>ES: 기존 문서 조회 (before)
    SVC->>ES: 문서 업데이트
    SVC->>AUD: ElasticsearchAuditLog.forUpdate(before, after)
    AUD->>DB: elasticsearch_audit_log INSERT
    SVC-->>FE: true

    U->>FE: "변경 이력" 탭 → 필터
    FE->>API: GET /admin/audit-logs?indexName=...&actionType=UPDATE
    API->>AUD: getAuditLogs(...)
    AUD->>DB: 감사 로그 페이지 조회
    AUD-->>FE: AuditLogPageResponse

백엔드 구현

계층 클래스 / 파일 역할
Controller AdminController (apps/backend/.../controller/AdminController.kt) /admin/** 회원·권한·결제·쿠폰·리포트·피드백 endpoint
Controller AdminElasticController (apps/backend/.../controller/AdminElasticController.kt) /admin/elastic/** — ES 인덱스/문서 CRUD
Controller AdminAuditLogController (apps/backend/.../controller/AdminAuditLogController.kt) /admin/audit-logs/** — ES 변경 이력 조회
Service AdminService (apps/backend/.../application/service/admin/AdminService.kt) 회원/결제/쿠폰/리포트 비즈니스 로직
Service AdminElasticService (apps/backend/.../application/service/admin/AdminElasticService.kt) ES CRUD + 감사 로그 발행
Service ElasticsearchAuditService (apps/backend/.../application/service/admin/ElasticsearchAuditService.kt) 감사 로그 조회
Service TurnFeedbackService (apps/backend/.../application/service/conversation/) 피드백 목록/통계 (어드민 화면에서 호출)
Domain (Entity) ElasticsearchAuditLog (apps/backend/.../application/domain/admin/ElasticsearchAuditLog.kt) CREATE/UPDATE/DELETE 작업 이력 (JSON before_data/after_data, 64KB truncate)
Domain (Enum) AuditActionType (같은 파일) CREATE / UPDATE / DELETE
Security SecurityConfig (apps/backend/.../common/config/SecurityConfig.kt) /admin/**hasAuthority("ADMIN") 강제

도메인 규칙

규칙 위치 값 / 설명
어드민 권한 게이트 SecurityConfig.kt:113 authorize.requestMatchers("/admin/**").hasAuthority("ADMIN")AccountRole.ADMIN이 아니면 403
계정 상태 전이 AdminController.changeAccountStatus()AdminService ACTIVATED / DEACTIVATED / WITHDRAWAL 세 상태로 변경 (AccountStatus.kt)
계정 권한 전이 AdminController.changeAccountRole() GUEST / USER / ADMIN 세 권한 (AccountRole.kt)
쿠폰 발급 AdminController.generateCoupon()AdminService.generateCoupon() UUID 앞 5글자 대문자 코드 자동 생성 (Coupon.createRandom())
쿠폰 삭제 DELETE /admin/coupons/{code} CouponStatus.USED이면 AlreadyUsedCouponException (AdminService.kt:240-245). 미사용 쿠폰만 삭제 가능
무통장 입금 승인 AdminController.confirmBankPayment() startDate 입력 시 멤버십 시작일 기준으로 Payment.status = CONFIRMED
현금영수증 발행 토글 PATCH /admin/payments/{paymentId}/cash-receipt AdminService.changeCashReceiptStatus() — 발행/미발행 토글
리포트 삭제 AdminService.deleteReport() soft delete (Report.delete()) — currentProcessREQUESTED/REFER_LAW/REFER_EXAM/REFER_COUNSEL/COMPLETE/VALIDATION_FAIL
ES 페이지 사이즈 한계 AdminElasticController.listDocuments() size.coerceIn(1, 100) — 100건 초과 요청 시 100으로 자동 cap
감사 로그 JSON 64KB 제한 ElasticsearchAuditLog.truncateIfNeeded() MAX_JSON_SIZE = 64 * 1024. vector 컬럼은 제외, 문자열은 1000자로 자르고 truncated=true
감사 로그 보존 (정책 미정) 만료/삭제 cron 없음 — 무기한 보존
피드백 평점 범위 TurnFeedback.validateRating() 1-5 (1=매우불만족, 5=매우만족)
키워드 백필 안전장치 AdminController.backfillKeywords() @Max(500) — 1회 호출당 최대 500건 (CLAUDE.md Issue #148 참조: ALB 60s timeout 회피 위해 100 단위 chunk 권장)

API 엔드포인트

회원 / 권한 / 결제 / 쿠폰 / 리포트 / 피드백 (AdminController)

Method Path 설명
GET /admin/accounts/list 전체 회원 페이지 조회 (이름/폰 검색)
GET /admin/accounts/{accountId}/payments 특정 회원의 결제·멤버십 이력
PATCH /admin/accounts/{accountId} 이름/폰 수정
GET /admin/accounts/roles 권한 관리 목록
PATCH /admin/accounts/{accountId}/role 권한 변경 (USER ↔ ADMIN ↔ GUEST)
PATCH /admin/accounts/{accountId}/status 상태 변경 (ACTIVATED / DEACTIVATED / WITHDRAWAL)
GET /admin/payments 결제 목록 (period, status, searchWord 필터)
GET /admin/payments/v2 Toss V2 결제 목록
PATCH /admin/payments/{paymentId}/bank-confirm 무통장 입금 확인
PATCH /admin/payments/{paymentId}/cash-receipt 현금영수증 발행 토글
GET /admin/coupons 쿠폰 목록
POST /admin/coupons 쿠폰 발급 (productType, amount)
DELETE /admin/coupons/{code} 쿠폰 코드 삭제
GET /admin/reports/list?category={LawCategory} 리포트 목록 (카테고리별)
DELETE /admin/reports/{reportId} 리포트 soft delete
GET /admin/feedbacks 사용자 피드백 목록 (minRating, maxRating 필터)
GET /admin/feedbacks/statistics 피드백 통계 (전체 개수, 평균 평점, 분포)
POST /admin/conversations/backfill-keywords 키워드 백필 (dryRun, limit ≤ 500)

ES 문서 관리 (AdminElasticController)

Method Path 설명
GET /admin/elastic/indices 전체 인덱스 + 문서 개수
GET /admin/elastic/{indexName}/documents 문서 페이지 (검색·정렬)
GET /admin/elastic/{indexName}/documents/{documentId} 단일 문서 조회
POST /admin/elastic/{indexName}/documents 문서 생성 (감사 로그 자동 발행)
PUT /admin/elastic/{indexName}/documents/{documentId} 문서 수정
DELETE /admin/elastic/{indexName}/documents/{documentId} 문서 삭제

감사 로그 (AdminAuditLogController)

Method Path 설명
GET /admin/audit-logs 감사 로그 페이지 (indexName, actionType, adminAccountId, startDate, endDate)
GET /admin/audit-logs/{auditId} 감사 로그 상세 (before/after 전체)
GET /admin/audit-logs/history?indexName=...&documentId=... 특정 문서의 변경 이력

스펙 상세(request/response schema)는 API 레퍼런스 / OpenAPI 페이지에서 endpoint별 검색.

데이터 모델

erDiagram
    ACCOUNT {
        string account_id PK
        string phone
        string name
        string role "GUEST / USER / ADMIN"
        string account_status "ACTIVATED / DEACTIVATED / WITHDRAWAL"
    }
    PAYMENT {
        string payment_id PK
        string account_id FK
        string status "REGISTERED / SCHEDULED / PRE_VERIFICATION / CONFIRMED / VERIFICATION_FAIL ..."
        boolean cash_receipt_published
    }
    COUPON {
        string code PK "UUID 앞 5글자 대문자"
        string product_type "ProductType enum"
        string status "UNUSED / USED"
        datetime used_time "nullable"
    }
    REPORT {
        long report_id PK
        string initial_question
        string current_process "REQUESTED / REFER_LAW / REFER_EXAM / REFER_COUNSEL / COMPLETE / VALIDATION_FAIL"
        string category "LawCategory"
        datetime created_date_time
    }
    TURN_FEEDBACK {
        long turn_feedback_id PK
        long turn_id FK
        int rating "1-5"
        string reason "nullable"
    }
    ELASTICSEARCH_AUDIT_LOG {
        long audit_id PK
        string action_type "CREATE / UPDATE / DELETE"
        string index_name
        string document_id
        string admin_account_id FK
        string before_data "JSON (≤64KB, vector 제외)"
        string after_data "JSON (≤64KB, vector 제외)"
        boolean truncated
    }
    ACCOUNT ||--o{ PAYMENT : "결제"
    ACCOUNT ||--o{ ELASTICSEARCH_AUDIT_LOG : "ES 작업 수행자"
    ACCOUNT ||--o{ REPORT : "작성자"

설정

항목 위치 비고
어드민 권한 게이트 SecurityConfig.kt:113 /admin/**hasAuthority("ADMIN")
감사 로그 JSON 크기 한도 ElasticsearchAuditLog.MAX_JSON_SIZE 64KB. 초과 시 문자열 1000자로 truncate, truncated=true
ES 문서 페이지 사이즈 cap AdminElasticController.listDocuments() size.coerceIn(1, 100)
어드민 화면 frontend 라우트 apps/frontend/src/app/admin/page.tsx 단일 SPA, 탭: 회원/권한/주문/쿠폰/참조데이터/변경이력/백업
어드민 API 클라이언트 (frontend) apps/frontend/src/service/adminAPI.ts, adminElasticAPI.ts TypeScript 타입 + axios 호출
키워드 백필 ALB timeout (인프라) ALB/Cloudflare 기본 60s — limit=100으로 chunk 호출 권장 (CLAUDE.md "Issue #148 — 인프라 미해결 사항 #4")

알려진 이슈 / 개선 예정

  • 감사 로그 보존 정책 미정: elasticsearch_audit_log 테이블에 만료/삭제 cron 없음 → 무기한 누적. ES 문서 수정이 잦아지면 테이블이 비대해질 가능성.
  • 키워드 백필 동기 처리: POST /admin/conversations/backfill-keywords는 LLM을 동기 호출 (~500ms × N건). limit=500이면 ~4분 — ALB 60s timeout을 넘어 클라이언트는 응답을 못 받지만 백엔드는 계속 실행 (CLAUDE.md "Issue #148 — 인프라 미해결 사항 #4"). 별도 백그라운드 job 또는 chunked invocation 필요.
  • 쿠폰 삭제는 미사용 쿠폰만: DELETE /admin/coupons/{code}CouponStatus.USED인 쿠폰을 AlreadyUsedCouponException으로 거부 (AdminService.kt:240-245). 사용 이력 보존 정책. (이전 위키 초안에 정반대로 적혔던 부분 정정)
  • /admin/payments/admin/payments/v2 병행 운영: PortOne V1 → Toss V2 마이그레이션(Issue #109) 잔재. Toss V2가 prod 안정화되면 v1 endpoint 정리 필요.
  • 어드민 작업 감사 로그는 ES CRUD에만 적용: 회원 권한/상태 변경, 쿠폰 발급, 결제 승인은 별도 감사 추적 없음. 민감 작업에 대한 audit trail 보강 여지.
  • @Async SimpleAsyncTaskExecutor 사용 (해결됨): KeywordExtractionListener 등이 burst 시 thread 폭주 위험이었으나, AsyncConfig.kt에 bounded ThreadPoolTaskExecutor가 적용됨 (CLAUDE.md "Audit에서 해결된 항목").
  • 소프트 삭제만 적용된 리포트: AdminService.deleteReport()는 entity의 delete() 호출 — 물리 삭제 아님. 페이지 조회 쿼리에서 deleted flag 필터링 필요.

관련 문서