diff --git a/CLAUDE.md b/CLAUDE.md index c518e35..f9f2187 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,56 +1,308 @@ # gc-guide-api — 가이드 사이트 백엔드 API ## 프로젝트 개요 -gc-guide 프론트엔드의 백엔드 API. Google OAuth2 인증, 사용자 관리, 이슈 트래커 제공. +gc-guide 프론트엔드의 백엔드 API. Google OAuth2 인증, 사용자 관리(RBAC), 활동 기록, 이슈 트래커 제공. ## 기술 스택 -- Spring Boot 3.5 + JDK 17 -- Spring Security (JWT 기반 인증) +- Spring Boot 3.5.2 + JDK 17 +- Spring Security (JWT 기반 Stateless 인증) - Spring Data JPA + PostgreSQL (운영) / H2 (로컬) -- Google API Client (ID Token 검증) +- Google API Client 2.7.2 (ID Token 검증) +- JJWT 0.12.6 (JWT 생성/검증) - Lombok ## 빌드 & 실행 ```bash +source ~/.sdkman/bin/sdkman-init.sh && sdk use java 17.0.18-amzn # JDK 전환 ./mvnw -s .mvn/settings.xml clean compile # 컴파일 -./mvnw -s .mvn/settings.xml spring-boot:run # 로컬 실행 (H2) +./mvnw -s .mvn/settings.xml spring-boot:run # 로컬 실행 (H2, port 8080) ./mvnw -s .mvn/settings.xml package -DskipTests # JAR 패키징 ``` ## 프로필 -- `local` (기본): H2 인메모리 DB, 디버그 로깅 -- `prod`: PostgreSQL (gc_guide DB), validate 모드 - -## API 엔드포인트 - -### 인증 -- `POST /api/auth/google` — Google ID Token 검증 → JWT 발급 -- `GET /api/auth/me` — 현재 사용자 정보 -- `POST /api/auth/logout` — 세션 무효화 - -### 활동 기록 -- `GET /api/activity/login-history` — 로그인 이력 -- `POST /api/activity/track` — 페이지 조회 기록 - -### 이슈 관리 -- `GET/POST /api/issues` — 이슈 목록/생성 -- `GET/PUT /api/issues/:id` — 이슈 상세/수정 -- `POST /api/issues/:id/comments` — 코멘트 추가 - -### 관리자 -- `GET /api/admin/users` — 사용자 목록 -- `GET /api/admin/stats` — 통계 - -## DB (운영) -- Host: 211.208.115.83:5432 -- Database: gc_guide -- Username: gcguide +- `local` (기본): H2 인메모리 DB (`create-drop`), 디버그 로깅, H2 콘솔 `/h2-console` +- `prod`: PostgreSQL (gc_guide DB), `validate` 모드 ## 배포 -- Docker (eclipse-temurin:17-jre) -- guide.gc-si.dev/api/* → Nginx 프록시 +- Docker (eclipse-temurin:17-jre + JAR) +- guide.gc-si.dev/api/* → Nginx 프록시 → 컨테이너 +- CI/CD: Gitea Actions (main 머지 시 자동 빌드/배포) +- Gitea: https://gitea.gc-si.dev/gc/gc-guide-api + +## 의존성 레포지토리 +- Maven: Nexus 프록시 `.mvn/settings.xml` (admin/Gcsc!8932 인증 포함) ## 관련 프로젝트 -- gc-guide: 프론트엔드 (React + TypeScript) -- Gitea: https://gitea.gc-si.dev/gc/gc-guide-api +- gc-guide: 프론트엔드 (React + TypeScript + Vite) + +--- + +## 현재 구현 상태 + +### 완료 (scaffold) +- Spring Boot 프로젝트 초기화 (pom.xml + Maven Wrapper) +- application.yml + local/prod 프로필 분리 +- SecurityConfig.java — CSRF 비활성화, Stateless 세션, 공개 엔드포인트 설정 +- HealthController.java — `GET /api/health` → `{status: "UP"}` +- Nexus 프록시 인증 설정 (.mvn/settings.xml) +- 빌드 검증: `mvn clean compile` 성공 + +### 미구현 (별도 세션에서 작업) +아래 순서대로 구현 필요: + +#### 1단계: DB 엔티티 + JPA +패키지: `com.gcsc.guide.entity` + +```java +@Entity User — id, email, name, avatarUrl, status(PENDING/ACTIVE/REJECTED/DISABLED), isAdmin, createdAt, updatedAt, lastLoginAt +@Entity Role — id, name, description, createdAt +@Entity UserRole — userId, roleId (다대다, @IdClass 또는 @JoinTable) +@Entity RoleUrlPattern — id, roleId, urlPattern, createdAt +@Entity LoginHistory — id, userId, loginAt, ipAddress, userAgent +@Entity PageView — id, userId, pagePath, viewedAt +@Entity Issue — id, title, body, status, priority, authorId, assigneeId, createdAt, updatedAt +@Entity IssueComment — id, issueId, authorId, body, createdAt +``` + +패키지: `com.gcsc.guide.repository` — 각 엔티티별 JpaRepository + +#### 2단계: 인증 API +패키지: `com.gcsc.guide.auth` + +- `JwtTokenProvider.java` — JWT 생성/검증 (JJWT 0.12.6) + - secret: `app.jwt.secret` (256bit 이상) + - expiration: `app.jwt.expiration-ms` (기본 24시간) +- `JwtAuthenticationFilter.java` — OncePerRequestFilter, Authorization Bearer 토큰 파싱 +- `GoogleTokenVerifier.java` — Google API Client로 ID Token 검증 + - Client ID: `app.google.client-id` + - 이메일 도메인 검증: `app.allowed-email-domain` (gcsc.co.kr) +- `AuthController.java`: + - `POST /api/auth/google` — { idToken } → GoogleTokenVerifier → User 조회/생성 → JWT 발급 + - 신규 사용자: status=PENDING, DB 저장 + - htlee@gcsc.co.kr: 자동 ACTIVE + isAdmin=true + - 응답: { token, user } + - `GET /api/auth/me` — JWT에서 userId 추출 → User + roles 반환 + - `POST /api/auth/logout` — (Stateless이므로 프론트에서 토큰 삭제, 서버는 204) + +SecurityConfig 수정: +- JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 앞에 추가 +- 공개 엔드포인트: `/api/auth/**`, `/api/health`, `/actuator/health`, `/h2-console/**` + +#### 3단계: 관리자 API +패키지: `com.gcsc.guide.controller` + +- `AdminUserController.java`: + - `GET /api/admin/users` — 전체 목록 (status 필터 쿼리파라미터) + - `PUT /api/admin/users/{id}/approve` — PENDING→ACTIVE + - `PUT /api/admin/users/{id}/reject` — PENDING→REJECTED + - `PUT /api/admin/users/{id}/disable` — ACTIVE→DISABLED + - `PUT /api/admin/users/{id}/roles` — { roleIds: [1,2] } → user_roles 업데이트 + - `POST /api/admin/users/{id}/admin` — isAdmin=true + - `DELETE /api/admin/users/{id}/admin` — isAdmin=false + +- `AdminRoleController.java`: + - `GET /api/admin/roles` — 전체 롤 목록 + - `POST /api/admin/roles` — { name, description } → Role 생성 + - `PUT /api/admin/roles/{id}` — 수정 + - `DELETE /api/admin/roles/{id}` — 삭제 + - `GET /api/admin/roles/{id}/permissions` — URL 패턴 목록 + - `POST /api/admin/roles/{id}/permissions` — { urlPattern } 추가 + - `DELETE /api/admin/permissions/{id}` — 삭제 + +- `AdminStatsController.java`: + - `GET /api/admin/stats` — { totalUsers, activeUsers, pendingUsers, todayLogins, ... } + +관리자 권한 체크: `@PreAuthorize("@authChecker.isAdmin()")` 또는 SecurityConfig에서 `/api/admin/**` 패턴에 커스텀 필터 적용 + +#### 4단계: 활동 기록 API +- `ActivityController.java`: + - `POST /api/activity/track` — { pagePath } → PageView 저장 + - `GET /api/activity/login-history` — 현재 사용자의 로그인 이력 + +#### 5단계: 이슈 관리 API +- `IssueController.java`: + - `GET /api/issues` — 이슈 목록 (status 필터, 페이징) + - `POST /api/issues` — { title, body, priority } → Issue 생성 + - `GET /api/issues/{id}` — 이슈 상세 (코멘트 포함) + - `PUT /api/issues/{id}` — 수정 + - `POST /api/issues/{id}/comments` — { body } → 코멘트 추가 + +#### 6단계: 롤 기반 URL 접근 제어 (선택) +- `DynamicAuthorizationManager.java` — 커스텀 AuthorizationManager + - 매 요청마다 DB에서 사용자 롤 → role_url_patterns 조회 → ant-style 매칭 + - SecurityConfig에서 `.anyRequest().access(dynamicAuthorizationManager)` 적용 + - 캐싱: 사용자별 URL 패턴을 JWT 클레임에 포함하거나 Redis/로컬캐시 활용 + +#### 7단계: 초기 데이터 시딩 +`data.sql` 또는 ApplicationRunner로: + +```sql +INSERT INTO roles (name, description) VALUES + ('ADMIN', '전체 접근 권한 (관리자 페이지 포함)'), + ('DEVELOPER', '전체 개발 가이드 접근'), + ('FRONT_DEV', '프론트엔드 개발 가이드만'); + +INSERT INTO role_url_patterns (role_id, url_pattern) VALUES + (1, '/**'), + (2, '/dev/**'), + (3, '/dev/front/**'); +``` + +--- + +## DB 스키마 + +### 로컬 (H2, create-drop) +JPA가 엔티티 기반으로 자동 생성. `data.sql`로 초기 시드. + +### 운영 (PostgreSQL) +| 항목 | 값 | +|------|-----| +| Host | 211.208.115.83 (Docker: 172.17.0.1) | +| Port | 5432 | +| Database | gc_guide | +| Username | gcguide | +| Password | GcGuide!2026 | + +**DB/사용자 아직 미생성** — 서버에서 아래 SQL 실행 필요: + +```sql +CREATE USER gcguide WITH PASSWORD 'GcGuide!2026'; +CREATE DATABASE gc_guide OWNER gcguide ENCODING 'UTF8'; +GRANT ALL PRIVILEGES ON DATABASE gc_guide TO gcguide; +``` + +스키마 DDL (JPA가 로컬에서 자동 생성하되, 운영은 validate 모드이므로 수동 생성 필요): + +```sql +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + avatar_url VARCHAR(500), + status VARCHAR(20) DEFAULT 'PENDING', + is_admin BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + last_login_at TIMESTAMP +); + +CREATE TABLE roles ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + description VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE user_roles ( + user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, + role_id BIGINT REFERENCES roles(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, role_id) +); + +CREATE TABLE role_url_patterns ( + id BIGSERIAL PRIMARY KEY, + role_id BIGINT REFERENCES roles(id) ON DELETE CASCADE, + url_pattern VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE login_history ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT REFERENCES users(id), + login_at TIMESTAMP DEFAULT NOW(), + ip_address VARCHAR(45), + user_agent VARCHAR(500) +); + +CREATE TABLE page_views ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT REFERENCES users(id), + page_path VARCHAR(255) NOT NULL, + viewed_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE issues ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + body TEXT, + status VARCHAR(20) DEFAULT 'OPEN', + priority VARCHAR(20) DEFAULT 'NORMAL', + author_id BIGINT REFERENCES users(id), + assignee_id BIGINT REFERENCES users(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE issue_comments ( + id BIGSERIAL PRIMARY KEY, + issue_id BIGINT REFERENCES issues(id) ON DELETE CASCADE, + author_id BIGINT REFERENCES users(id), + body TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_login_history_user ON login_history(user_id); +CREATE INDEX idx_page_views_user ON page_views(user_id); +CREATE INDEX idx_issues_status ON issues(status); +CREATE INDEX idx_issue_comments_issue ON issue_comments(issue_id); +``` + +--- + +## 패키지 구조 (목표) + +``` +com.gcsc.guide/ +├── GcGuideApiApplication.java ✅ +├── config/ +│ └── SecurityConfig.java ✅ (JWT 필터 추가 필요) +├── auth/ +│ ├── JwtTokenProvider.java ⬜ +│ ├── JwtAuthenticationFilter.java ⬜ +│ ├── GoogleTokenVerifier.java ⬜ +│ └── AuthController.java ⬜ +├── entity/ +│ ├── User.java ⬜ +│ ├── Role.java ⬜ +│ ├── RoleUrlPattern.java ⬜ +│ ├── LoginHistory.java ⬜ +│ ├── PageView.java ⬜ +│ ├── Issue.java ⬜ +│ └── IssueComment.java ⬜ +├── repository/ ⬜ (각 엔티티별 JpaRepository) +├── service/ +│ ├── UserService.java ⬜ +│ ├── RoleService.java ⬜ +│ ├── ActivityService.java ⬜ +│ └── IssueService.java ⬜ +├── controller/ +│ ├── HealthController.java ✅ +│ ├── AdminUserController.java ⬜ +│ ├── AdminRoleController.java ⬜ +│ ├── AdminStatsController.java ⬜ +│ ├── ActivityController.java ⬜ +│ └── IssueController.java ⬜ +├── dto/ ⬜ (요청/응답 DTO) +└── exception/ ⬜ (GlobalExceptionHandler) +``` + +## application.yml 설정 참조 + +```yaml +app: + jwt: + secret: ${JWT_SECRET:gc-guide-dev-jwt-secret-key-must-be-at-least-256-bits-long} + expiration-ms: ${JWT_EXPIRATION:86400000} + google: + client-id: ${GOOGLE_CLIENT_ID:295080817934-1uqaqrkup9jnslajkl1ngpee7gm249fv.apps.googleusercontent.com} + allowed-email-domain: gcsc.co.kr +``` + +## 구현 시 주의사항 +- DTO와 Entity 분리 필수 (API 응답에 Entity 직접 노출 금지) +- 비즈니스 로직은 Service 계층에 집중 +- @Transactional 범위 최소화 +- H2 로컬 모드에서 전체 기능 테스트 가능하도록 프로필 분리 유지 +- Google OAuth2 Client ID는 gc-guide (프론트엔드)와 동일한 것 사용