관리자 콘솔
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()) — currentProcess는 REQUESTED/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 필터링 필요.
관련 문서