-
Notifications
You must be signed in to change notification settings - Fork 0
feat: User Service 인증/인가 + CRUD 전체 구현 #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
424ac3b
.
gh9727 092f374
feat: User Service 인증/인가 + CRUD 전체 구현
HellWorlding e1157c8
fix: 코드래빗 리뷰 반영 (보상트랜잭션, softDelete, checkMasterId, 개인정보 제거, postgres…
HellWorlding d9b3522
fix: 코드래빗 리뷰 반영 (Jackson 역직렬화, client-secret, softDelete, 보상트랜잭션, 개인정보)
HellWorlding 22a39eb
fix: 코드래빗 추가 리뷰 반영 (계층분리 TokenData, HubInfo 검증, KeycloakProperties @V…
HellWorlding db8491a
fix: HubInfo HubProvider 반환값 필드 유효성 검증 추가
HellWorlding febe88c
fix: HubInfo 임시 생성자 null 체크 + TODO 검증 로직 주석
HellWorlding 7bbfad4
fix: 회원가입 시 MASTER 권한 직접 지정 차단
HellWorlding a80fa71
fix: MASTER 권한 체크 추가 (단건조회, 목록조회, 수정, 삭제)
HellWorlding 6e641ca
fix: softDelete/delete TODO 주석 추가 (SecurityUtil 전환, Outbox 정합성)
HellWorlding e9d6d89
fix: 보상 롤백 실패 시 원인 예외 보존 (withdraw 별도 try-catch)
HellWorlding b98ab2b
fix: deleteUser에 requesterId 전달 (삭제 감사 추적)
HellWorlding 08bd2dd
fix: updateUser에 @Valid 추가
HellWorlding 4f58d29
fix: delete() 협력자 null 검증 추가 (roleCheck, identityProvider)
HellWorlding 98bdecb
docs: README 전체 업데이트 (API 상태, 보안, 패키지 구조, 코드래빗 반영)
HellWorlding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,4 +35,5 @@ out/ | |
|
|
||
| ### VS Code ### | ||
| .vscode/ | ||
| .env* | ||
| .env* | ||
| kafka.server.truststore.jks | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| # Loopang User Service | ||
|
|
||
| loopang MSA 프로젝트의 유저 + 배송담당자 도메인 서비스. | ||
|
|
||
| ## 기술 스택 | ||
|
|
||
| - Java 21, Spring Boot 3.5.13, Spring Cloud 2025.0.1 | ||
| - Spring MVC (Servlet 기반) | ||
| - 공통 모듈: `com.loopang:common:0.0.4-SNAPSHOT` | ||
| - PostgreSQL 17, JPA + QueryDSL | ||
| - Keycloak (OAuth2 Resource Server, Admin Client) | ||
| - Redis (세션/캐싱) | ||
| - Eureka Client + Config Server | ||
| - Lombok, Gradle | ||
| - server.port: Config Server에서 관리 | ||
|
|
||
| ## 도메인 범위 | ||
|
|
||
| - **유저 관리** (회원가입, 조회, 수정, 삭제) — `p_user` | ||
| - **배송담당자 관리** — `p_courier` | ||
| - **인증** — Keycloak 연동 (토큰 발급, 회원가입, 탈퇴) | ||
|
|
||
| ## API 엔드포인트 | ||
|
|
||
| ### 인증 (✅ 구현 완료) | ||
|
|
||
| | 메서드 | URL | 설명 | 인증 | 상태 | | ||
| |---|---|---|---|---| | ||
| | POST | `/api/users` | 회원가입 (Keycloak + DB) | 불필요 | ✅ | | ||
| | POST | `/api/users/login` | 로그인 (JWT 토큰 발급) | 불필요 | ✅ | | ||
| | POST | `/api/users/logout` | 로그아웃 (토큰 무효화) | 불필요 | ✅ | | ||
|
|
||
| ### 유저 (구현 예정) | ||
|
|
||
| | 메서드 | URL | 설명 | 인증 | 상태 | | ||
| |---|---|---|---|---| | ||
| | GET | `/api/users/me` | 현재 유저 정보 | 필요 | ⬜ | | ||
| | GET | `/api/users/{userId}` | 유저 단건 조회 | MASTER | ⬜ | | ||
| | GET | `/api/users` | 유저 목록 조회 | MASTER | ⬜ | | ||
| | PATCH | `/api/users/{userId}` | 유저 권한/정보 수정 | MASTER | ⬜ | | ||
| | DELETE | `/api/users/{userId}` | 유저 삭제 (소프트) | MASTER | ⬜ | | ||
|
|
||
| ### 배송담당자 | ||
|
|
||
| | 메서드 | URL | 설명 | 인증 | | ||
| |---|---|---|---| | ||
| | GET | `/api/couriers` | 배송담당자 목록 | 필요 | | ||
| | PUT | `/api/couriers/{courierId}` | 배송순번 변경 | MASTER/HUB | | ||
|
|
||
| ## 테이블 구조 | ||
|
|
||
| ### p_user (유저) | ||
|
|
||
| | 컬럼 | 타입 | 제약 | 설명 | | ||
| |---|---|---|---| | ||
| | id | UUID | PK | 유저ID (Keycloak sub) | | ||
| | email | VARCHAR(50) | NOT NULL | 이메일 | | ||
| | name | VARCHAR(50) | NOT NULL | 이름 | | ||
| | slack_id | VARCHAR(100) | NOT NULL | 슬랙ID | | ||
| | role | VARCHAR(10) | NOT NULL | 권한 (MASTER/HUB/DELIVERY/COMPANY/PENDING) | | ||
| | hub_id | UUID | NOT NULL | 소속허브ID | | ||
| | hub_name | VARCHAR(50) | NOT NULL | 소속허브명 | | ||
| | company_id | UUID | NULL | 업체ID (업체 관련 사용자만) | | ||
| | company_name | VARCHAR(100) | NULL | 업체명 | | ||
| | approved | BOOLEAN | | 승인 여부 | | ||
| | version | INT | | 낙관적 락 (@Version) | | ||
| | + BaseUserEntity (createdAt/By, updatedAt/By, deletedAt/By) | | ||
|
|
||
| ### p_courier (배송담당자) | ||
|
|
||
| | 컬럼 | 타입 | 제약 | 설명 | | ||
| |---|---|---|---| | ||
| | courier_id | UUID | PK | 배송담당자ID | | ||
| | user_id | UUID | FK, NOT NULL | 유저ID | | ||
| | delivery_charge_type | VARCHAR(10) | NOT NULL | HUB / COMPANY | | ||
| | delivery_turn | INT | NOT NULL | 배송순번 | | ||
| | version | INT | | 낙관적 락 (@Version) | | ||
| | + BaseUserEntity | | ||
|
|
||
| ### 엔티티 특징 | ||
| - `@SQLRestriction("deleted_at IS NULL")` — 소프트 삭제된 데이터 조회 시 자동 제외 | ||
| - `@Version` — 낙관적 락 (동시 수정 방지) | ||
| - `BaseUserEntity` 상속 — createdAt/By, updatedAt/By, deletedAt/By 자동 관리 | ||
| - `HubInfo`, `CompanyInfo` — @Embeddable VO (Provider 통해 검증) | ||
|
|
||
| ## 클린 아키텍처 | ||
|
|
||
| ```text | ||
| com.loopang.userservice | ||
| ├── application/ # 서비스 레이어 | ||
| ├── domain/ # 도메인 레이어 | ||
| │ ├── entity/ | ||
| │ │ ├── User.java | ||
| │ │ └── Courier.java | ||
| │ ├── repository/ | ||
| │ │ ├── UserRepository.java | ||
| │ │ └── CourierRepository.java | ||
| │ ├── service/ # 도메인 서비스 인터페이스 | ||
| │ │ ├── IdentityProvider.java # Keycloak 연동 | ||
| │ │ ├── RoleCheck.java # 권한 체크 | ||
| │ │ ├── HubProvider.java # 허브 조회 (Feign) | ||
| │ │ └── CompanyProvider.java # 업체 조회 (Feign) | ||
| │ └── vo/ | ||
| │ ├── UserType.java # MASTER, HUB, DELIVERY, COMPANY, PENDING | ||
| │ ├── DeliveryChargeType.java # HUB, COMPANY | ||
| │ ├── HubInfo.java # @Embeddable | ||
| │ └── CompanyInfo.java # @Embeddable | ||
| ├── infrastructure/ # 인프라 구현 | ||
| │ ├── repository/ | ||
| │ │ ├── JpaUserRepository.java | ||
| │ │ └── JpaCourierRepository.java | ||
| │ ├── keycloak/ # Keycloak 구현 (TODO) | ||
| │ └── client/ # Feign Client (TODO) | ||
| ├── presentation/ # 컨트롤러 + DTO | ||
| │ └── dto/ | ||
| │ └── SignupRequestDto.java | ||
| └── config/ | ||
| ``` | ||
|
|
||
| ## 로컬 실행 | ||
|
|
||
| ### 사전 조건 | ||
| - Eureka Server (18761) | ||
| - Config Server (18888) | ||
| - PostgreSQL Docker | ||
| - Keycloak (3300) | ||
|
|
||
| ### 환경변수 | ||
| ```text | ||
| DB_URL=localhost:5432/user;DB_USERNAME=postgres;DB_PASSWORD=본인비밀번호 | ||
| KEYCLOAK_SERVER_URL=http://localhost:13300 | ||
| REALM=my-realm | ||
| KEYCLOAK_CLIENT_ID=loopang-client | ||
| KEYCLOAK_CLIENT_SECRET=본인시크릿 | ||
| ``` | ||
|
|
||
| ### Keycloak 로컬 실행 | ||
| ```bash | ||
| docker run -d --name keycloak -p 13300:13300 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:24.0.0 start-dev --http-port=13300 --http-enabled=true --hostname-strict=false | ||
|
|
||
| # SSL 끄기 (master + my-realm) | ||
| docker exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:13300 --realm master --user admin --password admin | ||
| docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/master -s sslRequired=NONE | ||
| docker exec -it keycloak /opt/keycloak/bin/kcadm.sh update realms/my-realm -s sslRequired=NONE --server http://localhost:13300 --realm master --user admin --password admin | ||
| ``` | ||
|
|
||
| ### Keycloak 설정 | ||
| - 관리 콘솔: `http://localhost:13300` (admin/admin) | ||
| - Realm: `my-realm` | ||
| - Client: `loopang-client` (Client authentication: On) | ||
|
|
||
| ## 구현 현황 | ||
|
|
||
| ### 완료 | ||
| - [x] User 엔티티 (@SQLRestriction, @Version, BaseUserEntity, HubInfo/CompanyInfo VO) | ||
| - [x] Courier 엔티티 (@SQLRestriction, @Version, User 1:1, DeliveryChargeType) | ||
| - [x] Repository 인터페이스 + JPA 구현 (existsByEmail, existsBySlackId) | ||
| - [x] Domain Service 인터페이스 (IdentityProvider, RoleCheck, HubProvider, CompanyProvider) | ||
| - [x] UserType, DeliveryChargeType enum | ||
| - [x] SignupRequestDto (@Valid) | ||
| - [x] Keycloak Admin Client 연동 (KeycloakIdentityProvider — register, login, logout, withdraw, changePassword) | ||
| - [x] KeycloakProperties (@ConfigurationProperties) | ||
| - [x] 회원가입 API (POST /api/users — Keycloak 등록 + DB 저장) | ||
| - [x] 로그인 API (POST /api/users/login — Keycloak 토큰 발급) | ||
| - [x] 로그아웃 API (POST /api/users/logout — refresh token 무효화) | ||
| - [x] UserService (signup, login, logout, getMe, getUser, getUsers, updateUser, deleteUser) | ||
| - [x] UserController 전체 CRUD + 인증 API | ||
| - [x] /me 엔드포인트 (GET /api/users/me — @RequestHeader 방식) | ||
| - [x] 사용자 단건 조회 (GET /api/users/{userId}) | ||
| - [x] 사용자 목록 조회 (GET /api/users — 페이징) | ||
| - [x] 사용자 수정 (PATCH /api/users/{userId}) | ||
| - [x] 사용자 삭제 (DELETE /api/users/{userId}) | ||
| - [x] UserResponseDto, UserUpdateRequestDto | ||
| - [x] User 엔티티 update() 메서드 추가 | ||
| - [x] 예외 분리 (UserErrorCode, UserNotFoundException, UserEmailDuplicateException, UserSlackIdDuplicateException) | ||
| - [x] 테이블 명세 맞춤 (name VARCHAR(50), slackId VARCHAR(100), courier_id, delivery_charge_type) | ||
|
|
||
| ### TODO | ||
| - [ ] 검색 필터 (keyword, role, user_id — QueryDSL) | ||
| - [ ] CourierService + CourierController | ||
| - [ ] SecurityUtil 전환 (common publish 후 팀 전체 일괄 전환) | ||
| - [ ] RoleCheck / @PreAuthorize 권한 체크 | ||
| - [ ] HubProvider 구현 (Feign Client → hub-service) | ||
| - [ ] CompanyProvider 구현 (Feign Client → company-service) | ||
| - [ ] Dockerfile + Docker 배포 | ||
| - [ ] GitHub Actions CI/CD |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,78 @@ | ||
| plugins { | ||
| id 'java' | ||
| id 'org.springframework.boot' version '3.5.13' | ||
| id 'io.spring.dependency-management' version '1.1.7' | ||
| id 'java' | ||
| id 'org.springframework.boot' version '3.5.13' | ||
| id 'io.spring.dependency-management' version '1.1.7' | ||
| } | ||
|
|
||
| group = 'com.loopang' | ||
| version = '0.0.1-SNAPSHOT' | ||
|
|
||
| java { | ||
| toolchain { | ||
| languageVersion = JavaLanguageVersion.of(21) | ||
| } | ||
| toolchain { | ||
| languageVersion = JavaLanguageVersion.of(21) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| configurations { | ||
| compileOnly { | ||
| extendsFrom annotationProcessor | ||
| } | ||
| compileOnly { | ||
| extendsFrom annotationProcessor | ||
| } | ||
| } | ||
|
|
||
| repositories { | ||
| mavenCentral() | ||
| mavenCentral() | ||
| maven { | ||
| url = uri("https://maven.pkg.github.com/MSA-Service-12th/common") | ||
| credentials { | ||
| username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_USERNAME") | ||
| password = project.findProperty("gpr.token") ?: System.getenv("GITHUB_TOKEN") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-redis' | ||
| implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' | ||
| implementation 'org.springframework.boot:spring-boot-starter-security' | ||
| implementation 'org.springframework.boot:spring-boot-starter-web' | ||
| compileOnly 'org.projectlombok:lombok' | ||
| runtimeOnly 'org.postgresql:postgresql' | ||
| annotationProcessor 'org.projectlombok:lombok' | ||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||
| testImplementation 'org.springframework.security:spring-security-test' | ||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||
| // 공통 모듈 | ||
| implementation 'com.loopang:common:0.0.4-SNAPSHOT' | ||
| //keycloak | ||
| implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' | ||
| implementation 'org.keycloak:keycloak-admin-client:24.0.0' | ||
| // eureka, config | ||
| implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' | ||
| implementation 'org.springframework.cloud:spring-cloud-starter-config' | ||
| //seucrity는 다시 비활성화해야함 | ||
| // implementation 'org.springframework.boot:spring-boot-starter-security' | ||
|
|
||
| //DB | ||
| runtimeOnly 'org.postgresql:postgresql' | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-redis' | ||
|
|
||
| //queryDSL & lombok | ||
| implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' | ||
| annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' | ||
| annotationProcessor 'jakarta.persistence:jakarta.persistence-api' | ||
| annotationProcessor 'org.projectlombok:lombok' | ||
| compileOnly 'org.projectlombok:lombok' | ||
|
|
||
| //test | ||
| testCompileOnly 'org.projectlombok:lombok' | ||
| testAnnotationProcessor 'org.projectlombok:lombok' | ||
| testRuntimeOnly 'com.h2database:h2' | ||
|
|
||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||
| testImplementation 'org.springframework.security:spring-security-test' | ||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||
| } | ||
| ext { | ||
| set('springCloudVersion', "2025.0.1") | ||
| } | ||
|
|
||
| tasks.named('test') { | ||
| useJUnitPlatform() | ||
| dependencyManagement { | ||
| imports { | ||
| mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" | ||
| } | ||
| } | ||
|
|
||
| tasks.named('test') { | ||
| useJUnitPlatform() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| services: | ||
| postgres: | ||
| image: postgres:latest | ||
| container_name: loopang-db | ||
| restart: always | ||
| environment: | ||
| POSTGRES_USER: ${DB_USERNAME} | ||
| POSTGRES_PASSWORD: ${DB_PASSWORD} | ||
| POSTGRES_DB: ${DB} | ||
| ports: | ||
| - "5432:5432" | ||
| volumes: | ||
| - ./postgres_data:/var/lib/postgresql | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| rootProject.name = 'userservice' | ||
| rootProject.name = 'userservice' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.