You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
신원, 권한(role), 소속 허브(HubInfo Embedded), 회원가입/로그인/로그아웃/CRUD
Courier
배송담당자 — User에 1:1 매핑, 운영 정보(deliveryChargeType, deliveryTurn), 배정 후보 조회
인증
Keycloak 연동 (토큰 발급, 회원가입, 탈퇴)
Courier owner는 user-service입니다. delivery-service는 자체 Courier 엔티티 없이 본 서비스의 Internal API를 Feign으로 호출해 후보를 받아 배정합니다.
📡 API 엔드포인트
🔐 인증
메서드
URL
설명
인증
권한
POST
/api/users
회원가입 (Keycloak + DB)
불필요
전체 (MASTER 제외)
POST
/api/users/login
로그인 (JWT 토큰 발급)
불필요
전체
POST
/api/users/logout
로그아웃 (refresh token 무효화)
불필요
전체
👤 유저 (외부 — /api/users/**)
메서드
URL
설명
권한
GET
/api/users/me
내 정보 (X-User-UUID 헤더)
본인
GET
/api/users/{userId}
단건 조회
MASTER
GET
/api/users?role=&hubId=&page=&size=
페이지네이션 + role/hubId 필터
MASTER
PATCH
/api/users/{userId}
수정
MASTER
DELETE
/api/users/{userId}
소프트 삭제
MASTER
👤 유저 (내부 — /internal/users/**)
메서드
URL
설명
GET
/internal/users/{userId}
단건 조회 (권한 검증 없음)
GET
/internal/users?role=&hubId=
리스트 조회 (페이지네이션 X, enabled=true 필터)
⚠️/internal/** 경로는 gateway 라우팅에서 제외되어 있습니다. 서비스 간 Feign 호출은 Eureka 디스커버리로 user-service 인스턴스에 직접 도달하므로 gateway 우회. 외부 클라이언트는 gateway를 통해 접근할 수 없으나, user-service 포드/인스턴스가 네트워크에서 직접 노출되면 무인증 엔드포인트가 그대로 열린다. ALB Listener Rule, Security Group, 또는 내부망 접근 제한 등 배포 시 네트워크 보안 설정이 적용된 환경을 전제로 한다.
🚚 배송담당자 (외부 — /api/couriers/**)
메서드
URL
설명
권한
POST
/api/couriers
등록 ({ userId, deliveryChargeType })
MASTER
GET
/api/couriers/{courierId}
단건 조회
MASTER
GET
/api/couriers?hubId=&type=&page=&size=
페이지네이션 + 필터
MASTER
PATCH
/api/couriers/{courierId}
타입 변경 (HUB ↔ COMPANY)
MASTER
DELETE
/api/couriers/{courierId}
소프트 삭제
MASTER
🚚 배송담당자 (내부 — /internal/couriers/**)
메서드
URL
설명
GET
/internal/couriers/{courierId}
단건 조회 (배송 알림용)
GET
/internal/couriers?hubId=&type=
라운드로빈 후보 리스트 (deliveryTurn ASC, enabled=true만)
🏗️ 도메인 / 데이터 모델
p_user — 유저
컬럼
타입
제약
설명
id
UUID
PK
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
HubInfo.hubId (소속 허브)
hub_name
VARCHAR(50)
NOT NULL
HubInfo.hubName
company_id
UUID
NULL
CompanyInfo.companyId (업체 사용자만)
company_name
VARCHAR(100)
NULL
CompanyInfo.companyName
approved
BOOLEAN
관리자 승인 여부
version
INT
낙관적 락 (@Version)
+ BaseUserEntity
createdAt/By, updatedAt/By, deletedAt/By
p_courier — 배송담당자
컬럼
타입
제약
설명
courier_id
UUID
PK
user_id
UUID
FK → p_user.id, NOT NULL
1:1 매핑
hub_id
UUID
NOT NULL
등록 시점 user 소속 허브 (denormalize)
hub_name
VARCHAR(50)
NOT NULL
등록 시점 hub-service 응답값
delivery_charge_type
VARCHAR(10)
NOT NULL
HUB / COMPANY
delivery_turn
INT
NOT NULL
배송 순번 (라운드로빈 정렬 키)
version
INT
낙관적 락
+ BaseUserEntity
유니크 제약: uk_courier_hub_type_turn (hub_id, delivery_charge_type, delivery_turn)
→ 동시 등록 시 race condition으로 같은 turn이 중복 할당되는 것을 DB 레벨에서 차단.
엔티티 특징
@SQLRestriction("deleted_at IS NULL") — soft-deleted 자동 제외
@Version — 낙관적 락 (동시 수정 방지)
BaseUserEntity 상속 — 감사 필드 자동 관리
HubInfo / CompanyInfo — @Embeddable VO
🔄 핵심 흐름
회원가입 (POST /api/users)
1. Request 검증 (이메일/슬랙ID 중복, MASTER 권한 차단)
2. HubProvider.get(hubId)
├─ FeignException.NotFound → NotFoundException(404)
├─ FeignException(5xx 등) → InternalServerException(500)
└─ 응답 hubId가 요청 hubId와 일치하는지 검증
3. Keycloak에 user 등록 (identityProvider.register)
4. p_user INSERT (HubInfo, CompanyInfo 포함)
⚠️ DB 저장 실패 시 보상 트랜잭션: identityProvider.withdraw로 Keycloak 롤백
5. SignupResponseDto 반환
배송담당자 등록 (POST /api/couriers)
1. user 조회 + 검증 (role=DELIVERY, enabled=true, hubInfo 필수)
2. 중복 등록 확인 (findByUser_Id)
3. HubProvider.get(user.hubInfo.hubId)
→ 회원가입 시점 이후 허브명이 바뀌었을 수 있으므로 hub-service에서 최신값 fetch
4. saveWithTurnRetry() — 동시성 방어
├─ findMaxDeliveryTurnIncludingDeleted(hubId, type) + 1 (soft-deleted 포함 monotonic)
├─ Courier.register(user, hubInfo, type, nextTurn) → saveAndFlush
└─ DataIntegrityViolationException catch 시 max+1 다시 읽고 재시도 (최대 5회)
5. 5회 모두 실패 시 InternalServerException
라운드로빈 후보 조회 (GET /internal/couriers)
delivery-service Feign 호출
↓
1. findAllByHubInfo_HubIdAndTypeOrderByDeliveryTurnAsc(hubId, type)
2. 소속 user.enabled=true 필터 (배정 부적합 제외)
3. CourierResponseDto 리스트 반환
↓
delivery-service: 라운드로빈으로 1명 선정 → 배송 레코드에 assignedCourierId 저장
🔌 외부 서비스 연동
Feign Client — HubFeignClient → hub-service
호출
용도
GET /api/hubs/{hubId}
회원가입/수정/Courier 등록 시 최신 hubName 조회
오류 처리 (HubProviderImpl):
케이스
변환
HTTP
FeignException.NotFound (404)
NotFoundException
404
200 응답 + data == null
NotFoundException
404
그 외 FeignException (5xx, 타임아웃, 연결 실패)
InternalServerException
500
응답 직렬화 실패 등
InternalServerException
500
200 응답 + 필수 필드 누락 (hubId/name null/blank)
InternalServerException
500
응답 hubId ≠ 요청 hubId
InternalServerException
500
Keycloak — KeycloakIdentityProvider
메서드
용도
register(email, password)
사용자 등록
login(email, password)
JWT 토큰 발급
logout(refreshToken)
refresh token 무효화
withdraw(userId)
사용자 탈퇴 (보상 트랜잭션 포함)
changePassword(userId, newPassword)
비밀번호 변경
🛡️ 보안
권한 체크 (현재)
외부 API: Controller에서 @RequestHeader("X-User-Role") + checkMaster()