Skip to content

[feat] 백오피스(admin-web) 구축 및 회원관리 기능 - #18

Merged
theminjunchoi merged 20 commits into
devfrom
feat/12-backoffice-member
Jul 22, 2026
Merged

[feat] 백오피스(admin-web) 구축 및 회원관리 기능#18
theminjunchoi merged 20 commits into
devfrom
feat/12-backoffice-member

Conversation

@theminjunchoi

@theminjunchoi theminjunchoi commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

개요

같은 레포에 백오피스(admin-web)를 SPA로 구축하고, 첫 기능으로 회원 관리를 붙였습니다.
인증은 구글 로그인 + 이메일 허용목록 방식이며, 로컬은 Firebase 없이 개발할 수 있습니다.

Closes #12

주요 변경

프론트엔드 (admin-web/)

  • React + TypeScript + Vite + Refine + shadcn/ui(흰색·모노크롬 세련 테마)
  • 로그인 / 사이드바 레이아웃 / 회원 목록 / 회원 상세
  • 회원 통계 대시보드: 지표 카드 + 상태 비율 도넛 게이지 + 가입 추이 그래프(recharts, 지연 로드)

백엔드 (/api/admin/**)

  • 관리자 인증: Firebase ID 토큰 검증 → 이메일이 허용목록(admin.emails)에 있으면 ROLE_ADMIN 토큰 발급. JWT에 admin 종류를 더해 인증 필터가 회원·관리자 토큰을 구분하고, /api/admin/**ROLE_ADMIN을 요구
  • 회원 조회: 목록(검색·상태 필터·페이지네이션), 상세
  • 회원 관리: 강제 탈퇴(POST /members/{id}/withdraw)
  • 통계: 전체·활성·탈퇴 수 + 최근 N일 가입 추이(GET /members/stats)

로컬 개발

  • mock 모드: @Profile("local") 전용 dev-login으로 Firebase 없이 허용목록 이메일로 로그인(dev/prod엔 이 엔드포인트 없음)
  • admin-web/dev.sh 한 줄로 MySQL·백엔드·프론트 기동

로컬 실행

./admin-web/dev.sh        # MySQL + 백엔드(:8080) + 프론트(:5173)
# http://localhost:5173 → "로컬 관리자로 로그인"

admin-web/.env.localVITE_AUTH_MODE=mock, VITE_DEV_ADMIN_EMAIL=<허용목록 이메일> 필요(.env.example 참고).

배포 관련 (별도 진행)

이 PR은 코드까지입니다. 실제 서빙은 배포 인프라(#8) 머지 후 아래를 와이어링합니다 — 절차는 admin-web/deploy/README.md:

  • admin.gamss.kr/dev-admin.gamss.kr 서브도메인(DNS는 이미 추가·전파됨) + nginx server 블록 + admin 컨테이너/CD
  • 배포 시 필요한 입력: Firebase 웹 SDK 설정(VITE_FIREBASE_*, 공개값), 관리자 이메일(ADMIN_EMAILS)

모습

스크린샷 2026-07-21 오전 2 15 25

검증

  • 백엔드: ktlint + 테스트 통과(관리자 인증·회원 검색/상태필터·탈퇴·통계 테스트 추가)
  • 프론트: build + lint 통과
  • 로컬 end-to-end 실측: dev-login/허용목록 거부(403)·무토큰(401), 목록·검색·상태필터, 탈퇴(재탈퇴 409), 통계 API

Summary by CodeRabbit

  • 신규 기능
    • 관리자 웹 콘솔을 추가했습니다.
    • Google 로그인 기반 관리자 인증과 로컬 개발용 관리자 로그인을 지원합니다.
    • 회원 목록(검색·상태 필터·페이징), 회원 상세 조회, 회원 탈퇴 처리를 제공합니다.
    • 전체/상태별 회원 수와 일별 가입 추이를 대시보드에서 시각화합니다.
  • 배포 및 문서
    • Docker·Nginx 기반 정적 SPA 배포 및 리버스 프록시 구성을 추가했습니다.
    • 로컬 개발/환경변수 예시 및 운영 배포 문서를 제공합니다.
  • 테스트
    • 관리자 인증·토큰·회원 검색/통계에 대한 테스트를 추가했습니다.

Vite + React + TypeScript + Refine 기반 백오피스 프로젝트를 초기화하고,
Tailwind + shadcn/ui(흰색·neutral 테마)로 공용 UI 컴포넌트를 구성한다.
경로 별칭(@/*)과 빌드 설정 포함.
회원 목록(검색·페이지네이션)과 상세 화면을 구현하고, 사이드바 레이아웃과
로그인 화면을 구성한다. 데이터·인증은 우선 목(mock)으로 연결한다.
SPA를 빌드해 nginx로 서빙하는 멀티스테이지 Dockerfile과 정적 서버 설정을 추가한다.
리버스 프록시 블록·compose·CD 연동은 deploy 참조 문서로 정리한다.
구글 로그인(Firebase ID 토큰)으로 확인한 이메일이 허용목록(admin.emails)에
있을 때만 ROLE_ADMIN 토큰을 발급한다. JWT에 admin 종류를 더해 인증 필터가
회원·관리자 토큰을 구분하고, /api/admin/** 는 ROLE_ADMIN 을 요구한다.
관리자용 회원 목록(검색·페이지네이션)과 상세 조회 엔드포인트를 추가한다.
회원 도메인을 재사용하고 이메일·닉네임 부분 검색을 지원한다.
목 인증·데이터를 걷어내고 구글 로그인(Firebase)으로 전환한다. 로그인 시 Firebase
ID 토큰을 백엔드 관리자 토큰으로 교환하며, 이때 허용목록 검증이 이뤄진다. 이후
/api/admin/** 를 관리자 토큰으로 호출하고, 만료(401) 시 세션으로 자동 재발급한다.
Firebase 설정은 빌드 시점 환경변수(VITE_*)로 주입한다.
Firebase 없이 허용목록 이메일로 관리자 토큰을 발급하는 dev-login 을 추가한다.
컨트롤러는 @Profile("local") 이고 보안 공개 경로도 local 프로필에서만 열려,
dev/prod 에는 이 엔드포인트가 존재하지 않는다.
Firebase 없이 로컬 개발이 가능하도록 mock 모드를 추가한다. VITE_AUTH_MODE=mock 이면
백엔드 dev-login 으로 로그인하고, Firebase는 지연 초기화해 설정이 없어도 앱이 뜬다.
vite dev 서버가 /api 를 로컬 백엔드(:8080)로 프록시한다.
MySQL·백엔드·프론트를 순서대로 띄우고, 백엔드는 헬스체크까지 기다린 뒤
프론트를 포그라운드로 실행한다. Ctrl+C 시 백엔드도 함께 종료된다.
모노크롬 세련 방향으로 다듬는다. 사이드바(브랜드·섹션 라벨·활성 상태·유저 블록),
상단 sticky 헤더(브레드크럼·로컬 모드 표시)를 정비하고, 회원 목록에 스켈레톤 로딩·
상태 배지(닷)·빈 상태·정제된 페이지네이션을 적용한다. 상세는 아바타 헤더와 구분선
정의 리스트로, 로그인은 은은한 배경과 카드 그림자로 개선한다. 공통 컴포넌트(아바타·
스켈레톤·페이지 헤더·상태 배지) 추가.
회원 목록에 status 필터를 더하고, 관리자가 회원을 탈퇴 처리하는
POST /api/admin/members/{id}/withdraw 를 추가한다(도메인 withdraw 재사용).
목록에 전체/활성/탈퇴 세그먼트 필터를 추가하고, 상세에서 확인 다이얼로그로
회원을 탈퇴 처리한다. dataProvider.custom 으로 탈퇴 API를 호출한다.
전체·활성·탈퇴 회원 수와 최근 N일 일자별 가입 추이(KST, 빈 날 0)를 집계하는
GET /api/admin/members/stats 를 추가한다.
회원 관리 상단에 지표 카드와 상태 비율 도넛 게이지, 가입 추이 영역 그래프를
recharts 로 시각화한다. 차트는 지연 로드해 초기·로그인 번들에서 제외한다.
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Test Results

180 tests  +16   180 ✅ +16   58s ⏱️ ±0s
 33 suites + 2     0 💤 ± 0 
 33 files   + 2     0 ❌ ± 0 

Results for commit df7231a. ± Comparison against base commit f2254e1.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Test Coverage

Overall Project 77.24% -6.47% 🍏
Files changed 62.16% 🍏

File Coverage
AdminAuthService.kt 100% 🍏
MemberStats.kt 100% 🍏
ErrorCode.kt 100% 🍏
JwtAuthenticationFilter.kt 100% 🍏
JwtAccessDeniedHandler.kt 100% 🍏
TokenType.kt 98.18% 🍏
SecurityConfig.kt 96.91% -3.09% 🍏
GlobalExceptionHandler.kt 96.63% -1.69% 🍏
AdminProperties.kt 93.55% -6.45% 🍏
JjwtIssuer.kt 90.4% -9.09% 🍏
MemberService.kt 83.61% -16.39% 🍏
DailySignup.kt 80% -20% 🍏
AdminPrincipal.kt 66.67% -33.33% 🍏
AdminAuthController.kt 22.22% -77.78% 🍏
AdminMemberController.kt 8.22% -91.78% 🍏
AdminDevLoginRequest.kt 0% 🍏
MemberStatsResponse.kt 0% 🍏
DailySignupResponse.kt 0% 🍏
PageResponse.kt 0% 🍏
AdminMeResponse.kt 0% 🍏
AdminTokenResponse.kt 0% 🍏
AdminLoginRequest.kt 0% 🍏
AdminDevAuthController.kt 0% 🍏

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@theminjunchoi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57c0673b-fe47-4a28-87bb-4a053014f447

📥 Commits

Reviewing files that changed from the base of the PR and between 37af59e and df7231a.

⛔ Files ignored due to path filters (1)
  • admin-web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • admin-web/README.md
  • admin-web/package.json
  • admin-web/src/App.tsx
  • admin-web/src/components/ui/skeleton.tsx
  • admin-web/src/lib/api.ts
  • admin-web/src/pages/members/list.tsx
  • admin-web/tailwind.config.js
  • src/main/kotlin/com/nexters/gamss/admin/config/AdminProperties.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/AdminDevAuthController.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/AdminMemberController.kt
  • src/main/kotlin/com/nexters/gamss/admin/service/AdminAuthService.kt
  • src/main/kotlin/com/nexters/gamss/global/exception/ErrorCode.kt
  • src/main/kotlin/com/nexters/gamss/global/exception/GlobalExceptionHandler.kt
  • src/main/kotlin/com/nexters/gamss/global/security/JjwtIssuer.kt
  • src/main/kotlin/com/nexters/gamss/global/security/JwtAccessDeniedHandler.kt
  • src/main/kotlin/com/nexters/gamss/global/security/SecurityConfig.kt
  • src/main/resources/application.yml
  • src/test/kotlin/com/nexters/gamss/global/security/SecurityIntegrationTest.kt
  • src/test/kotlin/com/nexters/gamss/member/service/MemberServiceTest.kt

Walkthrough

관리자 허용목록과 JWT 인증을 백엔드에 추가하고, 회원 검색·통계·상세·탈퇴 API를 구현했습니다. React/Refine 기반 admin SPA에는 인증, 회원관리 화면, 공통 UI, Docker·Nginx 배포 구성을 추가했습니다.

Changes

관리자 인증 및 보안

Layer / File(s) Summary
관리자 인증 계약과 JWT 보안
src/main/kotlin/com/nexters/gamss/admin/..., src/main/kotlin/com/nexters/gamss/global/security/...
관리자 이메일 허용목록, Google·로컬 로그인, 관리자 JWT 유형과 인증 주체를 추가하고 로컬 개발 로그인 경로를 제한했습니다.
인증 검증 테스트
src/test/kotlin/com/nexters/gamss/admin/..., src/test/kotlin/com/nexters/gamss/global/security/JjwtIssuerTest.kt
허용목록, 관리자 토큰 발급·파싱, 토큰 유형 불일치 동작을 검증합니다.

회원관리 API

Layer / File(s) Summary
회원 검색·통계 도메인
src/main/kotlin/com/nexters/gamss/member/...
회원 상태별 집계, 검색·상태 필터·페이징, KST 기준 일별 가입 통계를 추가했습니다.
회원관리 컨트롤러와 응답 DTO
src/main/kotlin/com/nexters/gamss/admin/controller/...
회원 목록·통계·상세·강제 탈퇴 API와 페이지·통계 응답 변환을 추가했습니다.
저장소·서비스 검증
src/test/kotlin/com/nexters/gamss/member/...
검색 조건, 상태 필터, 페이지 결과와 통계 날짜 범위를 검증합니다.

Admin SPA

Layer / File(s) Summary
Vite·Tailwind·공통 UI 기반
admin-web/package.json, admin-web/src/components/ui/*, admin-web/src/index.css, admin-web/vite.config.ts
React/Vite 프로젝트, Tailwind 스타일, 공통 버튼·카드·테이블·입력·대화상자 컴포넌트를 구성했습니다.
인증·API 연동과 라우팅
admin-web/src/App.tsx, admin-web/src/lib/*, admin-web/src/providers/*, admin-web/src/pages/login.tsx
Firebase 또는 mock 인증을 선택하고 관리자 토큰 교환·갱신, Refine 라우팅·데이터 프로바이더를 연결했습니다.
회원관리 화면
admin-web/src/components/layout/*, admin-web/src/pages/members/*
회원 목록 검색·필터·페이징, 통계 차트, 상세 조회와 확인 기반 탈퇴 처리를 구현했습니다.

개발 및 배포

Layer / File(s) Summary
컨테이너·프록시·로컬 실행
admin-web/Dockerfile, admin-web/nginx.conf, admin-web/deploy/*, admin-web/dev.sh
정적 SPA 멀티스테이지 이미지, Nginx 라우팅, 로컬 MySQL·백엔드·프론트엔드 통합 실행 절차를 추가했습니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 관리자
  participant LoginPage
  participant AuthProvider
  participant Firebase
  participant AdminAPI
  관리자->>LoginPage: 로그인 선택
  LoginPage->>AuthProvider: login()
  AuthProvider->>Firebase: Google 인증 또는 mock 로그인
  Firebase-->>AuthProvider: Firebase ID token
  AuthProvider->>AdminAPI: 관리자 토큰 교환
  AdminAPI-->>AuthProvider: 관리자 JWT
  AuthProvider-->>관리자: 회원 목록으로 이동
  관리자->>AdminAPI: 회원 검색·통계 요청
  AdminAPI-->>관리자: 목록·통계 응답
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 백엔드 관리 인증·회원 API와 배포용 Docker/nginx까지 들어가 있어 #12의 프론트 범위를 벗어납니다. 백엔드·배포 변경은 별도 PR로 분리하고, 이 PR에는 admin-web SPA와 목 로그인·회원 목록·상세만 유지하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 핵심 변경인 백오피스 구축과 회원관리 기능을 간결하게 담아 PR 내용과 잘 맞습니다.
Description check ✅ Passed 개요·주요 변경·로컬 실행·배포·검증이 포함되어 있어 템플릿을 대체로 충족합니다.
Linked Issues check ✅ Passed 요구된 SPA 스캐폴딩, 목 로그인, 회원 목록·상세, 빌드 가능한 구성은 모두 포함됩니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/12-backoffice-member

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@admin-web/dev.sh`:
- Around line 16-17: Update the cleanup function to stop using lsof on port 8080
and killing every matching process. Track the process started by the script via
backend_pid and use child-process termination such as pkill -P to terminate only
that process’s descendants, while preserving safe behavior when no backend
process exists.

In `@admin-web/README.md`:
- Around line 1-32: Replace the default Vite template content in the README with
project-specific documentation for the GAMSS admin backoffice. Include the
application purpose, prerequisites and runtime setup, development startup using
npm run dev, required .env configuration, and the main technology stack
including React, Refine, and Firebase.

In `@admin-web/src/App.tsx`:
- Around line 8-14: Move the authProvider declaration below all import
statements in App.tsx, keeping its existing mock-mode selection logic unchanged
and ensuring the module imports remain at the file top level.

In `@admin-web/src/components/ui/avatar.tsx`:
- Around line 1-16: Resolve the mismatch between the Avatar component and the
installed `@radix-ui/react-avatar` dependency: either refactor Avatar to use Radix
Avatar primitives, including its image and fallback behavior, or remove the
unused dependency from package.json if the existing text-only implementation is
intentional. Keep the chosen approach consistent with the component’s supported
API and current usage.

In `@admin-web/src/components/ui/skeleton.tsx`:
- Around line 3-5: Update the Skeleton component’s props to extend
React.HTMLAttributes<HTMLDivElement>, destructure className separately, and
forward the remaining props to the rendered div while preserving the existing
default classes and className merging.

In `@admin-web/src/index.css`:
- Line 37: Update the text-rendering declaration to use the lowercase
optimizelegibility value, preserving the existing property and behavior while
satisfying the Stylelint value-keyword-case rule.

In `@admin-web/src/lib/api.ts`:
- Around line 23-24: Update the authentication flow around the accessToken
assignment and TOKEN_KEY storage to avoid persisting the JWT in localStorage;
migrate token issuance and session handling to an HttpOnly, secure cookie
through the backend, and adjust the client API usage to rely on that cookie
instead of JavaScript-accessible token storage.
- Around line 64-67: Update refreshToken to cache the in-flight token-refresh
Promise and return that shared Promise to concurrent callers, regardless of
MOCK_MODE. Clear the cached Promise after completion, including rejection, so
later expired-token requests can retry normally while preserving the existing
devLoginToken and exchangeAdminToken behavior.
- Line 84: Update the response handling condition in the API request flow to
invoke refreshToken() only for 401 Unauthorized responses; return or propagate
403 Forbidden responses immediately without retrying or refreshing credentials.

In `@admin-web/src/pages/members/list.tsx`:
- Around line 75-83: 검색 입력의 즉시 상태와 목록 조회용 검색어를 분리하고, 조회용 값에는 약 300ms 디바운스를
적용하세요. Input의 onChange는 입력 상태만 갱신하고, 디바운스가 완료된 값으로 filters/useList가 요청되도록 해당 검색
상태와 조회 로직을 수정하세요.

In `@admin-web/src/pages/members/member-stats.tsx`:
- Around line 92-107: Prevent the zero-member case in the member stats hints
from displaying a misleading 100% withdrawn ratio. Update the hint values
associated with activeRatio and the withdrawn Metric in the surrounding stats
component so both use a neutral value such as "—" when total is zero, while
preserving the existing percentage calculations when total is greater than zero.

In `@admin-web/src/pages/members/show.tsx`:
- Around line 75-81: AlertDialogAction의 기본 즉시 닫힘 동작과 withdrawing 로딩 상태 처리를
일치시키세요. 로딩 상태를 사용자에게 표시하려는 의도라면 onWithdraw에서 이벤트 기본 동작을 막고, API 성공 시 제어된 다이얼로그
상태를 통해 닫도록 open/onOpenChange 흐름을 구현하세요. 즉시 닫기가 의도라면 AlertDialogAction의
withdrawing 기반 disabled 속성을 제거하세요.

In `@admin-web/tailwind.config.js`:
- Line 67: Update the Tailwind configuration’s plugins declaration to use a
static ESM import for tailwindcss-animate instead of require. Add the import at
the top of the configuration file, bind it to a named variable, and reference
that variable in the plugins array.

In `@src/main/kotlin/com/nexters/gamss/admin/config/AdminProperties.kt`:
- Around line 17-19: AdminProperties의 private normalizeEmail 로직이
AdminAuthService와 AdminDevAuthController에 중복되지 않도록 공통 재사용 경로를 제공하세요.
normalizeEmail의 가시성을 외부 호출 가능하게 변경하거나 공통 확장 함수로 분리한 뒤, 두 호출 지점이 해당 구현을 사용하도록
업데이트하고 기존 isAllowed 동작은 유지하세요.

In `@src/main/kotlin/com/nexters/gamss/admin/service/AdminAuthService.kt`:
- Around line 21-22: Update the authentication flow around
socialTokenVerifier.verify in AdminAuthService to validate that user.provider is
Google before accepting user.email. Reject non-Google providers with
BusinessException(ErrorCode.NOT_ADMIN), while preserving the existing
missing-email rejection.

In `@src/main/kotlin/com/nexters/gamss/global/security/JjwtIssuer.kt`:
- Around line 34-36: Update parseAccessToken and parseRefreshToken to use
nullable Long conversion via toLongOrNull instead of direct toLong, and map a
failed conversion to the existing INVALID_TOKEN domain exception. Preserve
successful numeric Member ID parsing for both access and refresh tokens.
- Line 65: Update the subject-returning logic in JjwtIssuer so a missing
claims.subject is handled explicitly with the established authentication-failure
exception path, rather than allowing Kotlin’s non-null return contract to fail
at runtime. Preserve returning the subject unchanged when it is present.

In `@src/main/kotlin/com/nexters/gamss/member/repository/MemberRepository.kt`:
- Around line 25-33: Retain the current partial-match query in the
MemberRepository search condition; no immediate code change is required. Record
the potential scalability concern for future optimization through full-text
search or a dedicated search engine if member data volume makes this query a
bottleneck.

In `@src/main/kotlin/com/nexters/gamss/member/service/MemberService.kt`:
- Around line 47-51: Replace the in-memory grouping in MemberService’s
countsByDate flow with a repository-level aggregation that returns date/count
results for the requested period and zone. Add the corresponding
findCreatedAtsSince aggregation method in memberRepository using the project’s
database query conventions, and map only the aggregated results into
countsByDate.

In `@src/test/kotlin/com/nexters/gamss/member/service/MemberServiceTest.kt`:
- Line 86: Replace the time-dependent Instant.now() values in the
findCreatedAtsSince stub within MemberServiceTest with a fixed, explicit instant
so the test no longer depends on execution time or date boundaries. Preserve the
existing repository return shape and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3415919-a8c5-41c2-84fb-c5f7773517ca

📥 Commits

Reviewing files that changed from the base of the PR and between 895feff and e2fc2c9.

⛔ Files ignored due to path filters (2)
  • admin-web/package-lock.json is excluded by !**/package-lock.json
  • admin-web/public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (74)
  • admin-web/.dockerignore
  • admin-web/.env.example
  • admin-web/.gitignore
  • admin-web/.oxlintrc.json
  • admin-web/Dockerfile
  • admin-web/README.md
  • admin-web/deploy/README.md
  • admin-web/deploy/gamss-admin.conf
  • admin-web/dev.sh
  • admin-web/index.html
  • admin-web/nginx.conf
  • admin-web/package.json
  • admin-web/postcss.config.js
  • admin-web/src/App.tsx
  • admin-web/src/components/layout/AdminLayout.tsx
  • admin-web/src/components/page-header.tsx
  • admin-web/src/components/status-badge.tsx
  • admin-web/src/components/ui/alert-dialog.tsx
  • admin-web/src/components/ui/avatar.tsx
  • admin-web/src/components/ui/badge.tsx
  • admin-web/src/components/ui/button.tsx
  • admin-web/src/components/ui/card.tsx
  • admin-web/src/components/ui/input.tsx
  • admin-web/src/components/ui/skeleton.tsx
  • admin-web/src/components/ui/table.tsx
  • admin-web/src/index.css
  • admin-web/src/lib/api.ts
  • admin-web/src/lib/firebase.ts
  • admin-web/src/lib/format.ts
  • admin-web/src/lib/utils.ts
  • admin-web/src/main.tsx
  • admin-web/src/pages/login.tsx
  • admin-web/src/pages/members/list.tsx
  • admin-web/src/pages/members/member-stats.tsx
  • admin-web/src/pages/members/show.tsx
  • admin-web/src/providers/authProvider.ts
  • admin-web/src/providers/dataProvider.ts
  • admin-web/src/providers/devAuthProvider.ts
  • admin-web/src/types/member.ts
  • admin-web/tailwind.config.js
  • admin-web/tsconfig.app.json
  • admin-web/tsconfig.json
  • admin-web/tsconfig.node.json
  • admin-web/vite.config.ts
  • src/main/kotlin/com/nexters/gamss/admin/config/AdminProperties.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/AdminAuthController.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/AdminDevAuthController.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/AdminMemberController.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/AdminDevLoginRequest.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/AdminLoginRequest.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/AdminMeResponse.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/AdminTokenResponse.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/DailySignupResponse.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/MemberStatsResponse.kt
  • src/main/kotlin/com/nexters/gamss/admin/controller/dto/PageResponse.kt
  • src/main/kotlin/com/nexters/gamss/admin/service/AdminAuthService.kt
  • src/main/kotlin/com/nexters/gamss/global/exception/ErrorCode.kt
  • src/main/kotlin/com/nexters/gamss/global/security/AdminPrincipal.kt
  • src/main/kotlin/com/nexters/gamss/global/security/JjwtIssuer.kt
  • src/main/kotlin/com/nexters/gamss/global/security/JwtAuthenticationFilter.kt
  • src/main/kotlin/com/nexters/gamss/global/security/JwtIssuer.kt
  • src/main/kotlin/com/nexters/gamss/global/security/SecurityConfig.kt
  • src/main/kotlin/com/nexters/gamss/global/security/TokenType.kt
  • src/main/kotlin/com/nexters/gamss/member/repository/MemberRepository.kt
  • src/main/kotlin/com/nexters/gamss/member/service/DailySignup.kt
  • src/main/kotlin/com/nexters/gamss/member/service/MemberService.kt
  • src/main/kotlin/com/nexters/gamss/member/service/MemberStats.kt
  • src/main/resources/application-local.yml
  • src/main/resources/application.yml
  • src/test/kotlin/com/nexters/gamss/admin/config/AdminPropertiesTest.kt
  • src/test/kotlin/com/nexters/gamss/admin/service/AdminAuthServiceTest.kt
  • src/test/kotlin/com/nexters/gamss/global/security/JjwtIssuerTest.kt
  • src/test/kotlin/com/nexters/gamss/member/repository/MemberRepositoryTest.kt
  • src/test/kotlin/com/nexters/gamss/member/service/MemberServiceTest.kt

Comment thread admin-web/dev.sh Outdated
Comment thread admin-web/README.md Outdated
Comment thread admin-web/src/App.tsx Outdated
Comment thread admin-web/src/components/ui/avatar.tsx
Comment thread admin-web/src/components/ui/skeleton.tsx Outdated
Comment thread src/main/kotlin/com/nexters/gamss/global/security/JjwtIssuer.kt Outdated
Comment thread src/main/kotlin/com/nexters/gamss/global/security/JjwtIssuer.kt Outdated
Comment thread src/main/kotlin/com/nexters/gamss/member/service/MemberService.kt
이메일만 믿으면 이메일 소유를 검증하지 않는 제공자로 관리자와 같은 주소를 만들어
권한을 탈취할 수 있어, 관리자는 구글 로그인만 허용하도록 검증을 추가한다.
- dev.sh: 8080 맹목적 종료 대신 백엔드 프로세스 그룹만 종료(무관한 프로세스 보호)
- 회원 통계: 회원 0명일 때 비율 힌트를 —로 표시(모순 문구 방지)
- 회원 상세: 즉시 닫히는 다이얼로그에서 무효한 disabled 속성 제거
- api: 401에만 토큰 재발급(403 제외), 동시 갱신 Promise 캐싱으로 중복 호출 방지
- 회원 목록 검색 300ms 디바운스
- import 순서 정리, Skeleton 표준 속성 전달, tailwind require→import
- 미사용 radix 의존성(avatar·dropdown-menu) 제거, README 프로젝트용으로 교체
- JjwtIssuer: subject null·비숫자 subject를 INVALID_TOKEN으로 매핑(500 방지)
- 이메일 정규화(AdminProperties.normalize)를 토큰 발급부에서 재사용
- 통계 테스트: 시간 의존 단언을 기간 합계로 바꿔 자정 경계 플래키 제거
@theminjunchoi
theminjunchoi requested a review from kite707 July 21, 2026 04:46
@kite707

kite707 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

회원 목록/통계 API에 size·days 파라미터 상한이 없어서 짚어봅니다.

  • AdminMemberController.list() (size, 기본 20) — 아주 큰 값을 주면 DB에서 대량 로우를 한 번에 끌어옵니다.
  • AdminMemberController.stats() (days, 기본 14) → MemberService.getStats()(0 until days).map { ... }days에 큰 값(예: 1억)을 주면 그 크기의 리스트를 만들다 서버가 멈추거나 OOM이 날 수 있습니다.

허용목록에 없으면 토큰 자체를 못 받으니 공격 표면은 좁지만, @Max 같은 검증을 추가하는 비용이 크지 않아서 상한을 걸어두면 좋을 것 같습니다.

(참고로 SecurityConfigAccessDeniedHandler가 따로 없어서, ROLE_ADMIN이 없는 인증된 사용자가 /api/admin/**을 호출하면 ApiResponse 포맷이 아닌 빈 바디로 403이 나갑니다 — 지금 admin-web은 이 케이스를 만들지 않아서 실사용 영향은 없지만, 컨벤션 일관성 차원에서 남겨둡니다.)

리뷰 반영:
- 회원 목록 size(<=100)·page(>=0), 통계 days(1~365)에 @Max/@min 상한 추가
  (@validated + ConstraintViolationException 을 400 INVALID_INPUT 으로 매핑)
- AccessDeniedHandler 추가 — ROLE_ADMIN 없는 인증 요청 403을 ApiResponse 포맷으로 반환
SecurityConfig 충돌 해소: PUBLIC_PATHS에 관리자 로그인(/api/admin/auth/login)과
dev의 /actuator/health 를 모두 유지. 나머지는 정상 자동 병합.
@theminjunchoi

Copy link
Copy Markdown
Collaborator Author

@kite707

  1. size·days 상한 — @validated + @Min/@max로 상한을 걸었습니다! size ≤ 100, days 1~365, page ≥ 0.
    특히 days는 (0 until days).map 때문에 큰 값이면 OOM 위험이 실제라 꼭 필요했을 것 같아요. 상한 위반이 500으로 안 떨어지도록 ConstraintViolationException → 400 INVALID_INPUT 매핑도 같이 추가했습니다.

  2. AccessDeniedHandler — 말씀대로 ROLE_ADMIN 없는 인증 요청의 403이 빈 바디로 나가고 있어서, JwtAccessDeniedHandler를 추가해서 ApiResponse 포맷(ACCESS_DENIED)으로 통일했습니다.

@kite707 kite707 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빠르시네요! 고생하셨습니다!

@theminjunchoi
theminjunchoi merged commit 427f582 into dev Jul 22, 2026
4 checks passed
@theminjunchoi
theminjunchoi deleted the feat/12-backoffice-member branch July 22, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 백오피스(admin-web) 구축 및 회원관리 기능

2 participants