kt-event-marketing-fe/design/backend/api/user-service-api.yaml
cherry2250 3f6e005026 초기 프로젝트 설정 및 설계 문서 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:10:16 +09:00

992 lines
30 KiB
YAML

openapi: 3.0.3
info:
title: User Service API
description: |
KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API
사용자 인증 및 매장정보 관리를 담당하는 마이크로서비스
**주요 기능:**
- 회원가입
- 로그인/로그아웃
- 프로필 조회 및 수정
- 비밀번호 변경
**보안:**
- JWT Bearer 토큰 기반 인증
- bcrypt 비밀번호 해싱
version: 1.0.0
contact:
name: Digital Garage Team
email: support@kt-event-marketing.com
servers:
- url: http://localhost:8081
description: Local Development Server
- url: https://dev-api.kt-event-marketing.com/user/v1
description: Development Server
- url: https://api.kt-event-marketing.com/user/v1
description: Production Server
tags:
- name: Authentication
description: 인증 관련 API (로그인, 로그아웃, 회원가입)
- name: Profile
description: 프로필 관련 API (조회, 수정, 비밀번호 변경)
paths:
/users/register:
post:
tags:
- Authentication
summary: 회원가입
description: |
소상공인 회원가입 API
**유저스토리:** UFR-USER-010
**주요 기능:**
- 기본 정보 및 매장 정보 등록
- 비밀번호 bcrypt 해싱
- JWT 토큰 자동 발급
**처리 흐름:**
1. 중복 사용자 확인 (전화번호 기반)
2. 비밀번호 해싱 (bcrypt)
3. User/Store 데이터베이스 트랜잭션 처리
4. JWT 토큰 생성 및 세션 저장 (Redis)
operationId: registerUser
x-user-story: UFR-USER-010
x-controller: UserController
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
examples:
restaurant:
summary: 음식점 회원가입 예시
value:
name: 홍길동
phoneNumber: "01012345678"
email: hong@example.com
password: "Password123!"
storeName: 맛있는집
industry: 음식점
address: 서울시 강남구 테헤란로 123
businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
cafe:
summary: 카페 회원가입 예시
value:
name: 김철수
phoneNumber: "01087654321"
email: kim@example.com
password: "SecurePass456!"
storeName: 아메리카노 카페
industry: 카페
address: 서울시 서초구 서초대로 456
businessHours: "매일 09:00-20:00"
responses:
'201':
description: 회원가입 성공
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterResponse'
examples:
success:
summary: 회원가입 성공 응답
value:
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
userId: 123
userName: 홍길동
storeId: 456
storeName: 맛있는집
'400':
description: 잘못된 요청
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
duplicateUser:
summary: 중복 사용자
value:
code: USER_001
message: 이미 가입된 전화번호입니다
timestamp: 2025-10-22T10:30:00Z
validationError:
summary: 입력 검증 오류
value:
code: VALIDATION_ERROR
message: 비밀번호는 8자 이상이어야 합니다
timestamp: 2025-10-22T10:30:00Z
'500':
description: 서버 오류
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/users/login:
post:
tags:
- Authentication
summary: 로그인
description: |
소상공인 로그인 API
**유저스토리:** UFR-USER-020
**주요 기능:**
- 전화번호/비밀번호 인증
- JWT 토큰 발급
- Redis 세션 저장
- 최종 로그인 시각 업데이트 (비동기)
**보안:**
- Timing Attack 방어 (에러 메시지 통일)
- bcrypt 비밀번호 검증
- JWT 토큰 7일 만료
operationId: loginUser
x-user-story: UFR-USER-020
x-controller: UserController
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
examples:
default:
summary: 로그인 요청 예시
value:
phoneNumber: "01012345678"
password: "Password123!"
responses:
'200':
description: 로그인 성공
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
examples:
success:
summary: 로그인 성공 응답
value:
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
userId: 123
userName: 홍길동
role: OWNER
email: hong@example.com
'401':
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
authFailed:
summary: 인증 실패
value:
code: AUTH_001
message: 전화번호 또는 비밀번호를 확인해주세요
timestamp: 2025-10-22T10:30:00Z
/users/logout:
post:
tags:
- Authentication
summary: 로그아웃
description: |
로그아웃 API
**유저스토리:** UFR-USER-040
**주요 기능:**
- Redis 세션 삭제
- JWT 토큰 Blacklist 추가
- 멱등성 보장
**처리 흐름:**
1. JWT 토큰 검증
2. Redis 세션 삭제
3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정)
4. 로그아웃 이벤트 발행
operationId: logoutUser
x-user-story: UFR-USER-040
x-controller: UserController
security:
- BearerAuth: []
responses:
'200':
description: 로그아웃 성공
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutResponse'
examples:
success:
summary: 로그아웃 성공 응답
value:
success: true
message: 안전하게 로그아웃되었습니다
'401':
description: 인증 실패 (유효하지 않은 토큰)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
invalidToken:
summary: 유효하지 않은 토큰
value:
code: AUTH_002
message: 유효하지 않은 토큰입니다
timestamp: 2025-10-22T10:30:00Z
/users/profile:
get:
tags:
- Profile
summary: 프로필 조회
description: |
사용자 프로필 조회 API
**유저스토리:** UFR-USER-030
**조회 정보:**
- 기본 정보 (이름, 전화번호, 이메일)
- 매장 정보 (매장명, 업종, 주소, 영업시간)
operationId: getProfile
x-user-story: UFR-USER-030
x-controller: UserController
security:
- BearerAuth: []
responses:
'200':
description: 프로필 조회 성공
content:
application/json:
schema:
$ref: '#/components/schemas/ProfileResponse'
examples:
success:
summary: 프로필 조회 성공 응답
value:
userId: 123
userName: 홍길동
phoneNumber: "01012345678"
email: hong@example.com
role: OWNER
storeId: 456
storeName: 맛있는집
industry: 음식점
address: 서울시 강남구 테헤란로 123
businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
createdAt: 2025-09-01T10:00:00Z
lastLoginAt: 2025-10-22T09:00:00Z
'401':
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: 사용자를 찾을 수 없음
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
notFound:
summary: 사용자 없음
value:
code: USER_003
message: 사용자를 찾을 수 없습니다
timestamp: 2025-10-22T10:30:00Z
put:
tags:
- Profile
summary: 프로필 수정
description: |
사용자 프로필 수정 API
**유저스토리:** UFR-USER-030
**수정 가능 항목:**
- 기본 정보: 이름, 전화번호, 이메일
- 매장 정보: 매장명, 업종, 주소, 영업시간
**주의사항:**
- 비밀번호 변경은 별도 API 사용 (/users/password)
- 전화번호 변경 시 향후 재인증 필요 (현재는 직접 변경 가능)
- Optimistic Locking으로 동시성 제어
operationId: updateProfile
x-user-story: UFR-USER-030
x-controller: UserController
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateProfileRequest'
examples:
fullUpdate:
summary: 전체 정보 수정
value:
name: 홍길동
phoneNumber: "01012345678"
email: hong.new@example.com
storeName: 맛있는집 (리뉴얼)
industry: 퓨전음식점
address: 서울시 강남구 테헤란로 456
businessHours: "매일 11:00-23:00"
partialUpdate:
summary: 일부 정보 수정 (이메일, 영업시간)
value:
email: hong.updated@example.com
businessHours: "월-금 10:00-22:00, 토-일 휴무"
responses:
'200':
description: 프로필 수정 성공
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateProfileResponse'
examples:
success:
summary: 프로필 수정 성공 응답
value:
userId: 123
userName: 홍길동
email: hong.new@example.com
storeId: 456
storeName: 맛있는집 (리뉴얼)
'400':
description: 잘못된 요청
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: 사용자를 찾을 수 없음
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: 동시성 충돌 (다른 세션에서 수정)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
conflict:
summary: 동시성 충돌
value:
code: USER_005
message: 다른 세션에서 프로필을 수정했습니다. 새로고침 후 다시 시도하세요
timestamp: 2025-10-22T10:30:00Z
/users/password:
put:
tags:
- Profile
summary: 비밀번호 변경
description: |
비밀번호 변경 API
**유저스토리:** UFR-USER-030
**주요 기능:**
- 현재 비밀번호 확인 필수
- 새 비밀번호 규칙 검증 (8자 이상, 영문/숫자/특수문자 포함)
- bcrypt 해싱
**보안:**
- 현재 비밀번호 검증 실패 시 400 Bad Request
- 비밀번호 변경 후 기존 세션 유지 (로그아웃 불필요)
operationId: changePassword
x-user-story: UFR-USER-030
x-controller: UserController
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChangePasswordRequest'
examples:
default:
summary: 비밀번호 변경 요청
value:
currentPassword: "Password123!"
newPassword: "NewSecurePass456!"
responses:
'200':
description: 비밀번호 변경 성공
content:
application/json:
schema:
$ref: '#/components/schemas/ChangePasswordResponse'
examples:
success:
summary: 비밀번호 변경 성공 응답
value:
success: true
message: 비밀번호가 성공적으로 변경되었습니다
'400':
description: 현재 비밀번호 불일치 또는 새 비밀번호 규칙 위반
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
invalidCurrentPassword:
summary: 현재 비밀번호 불일치
value:
code: USER_004
message: 현재 비밀번호가 일치하지 않습니다
timestamp: 2025-10-22T10:30:00Z
invalidNewPassword:
summary: 새 비밀번호 규칙 위반
value:
code: VALIDATION_ERROR
message: 비밀번호는 8자 이상이어야 하며 영문/숫자/특수문자를 포함해야 합니다
timestamp: 2025-10-22T10:30:00Z
'401':
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/users/{userId}/store:
get:
tags:
- Profile
summary: 매장정보 조회 (서비스 연동용)
description: |
특정 사용자의 매장정보를 조회하는 API (내부 서비스 연동용)
**사용 목적:**
- Event Service에서 이벤트 생성 시 매장정보 조회
- Content Service에서 매장정보 기반 콘텐츠 생성
- Service-to-Service 통신용 내부 API
**주의사항:**
- Internal API로 외부 노출 금지
- API Gateway에서 인증된 서비스만 접근 허용
- 매장정보는 Redis 캐시 우선 조회 (TTL 30분)
operationId: getStoreByUserId
x-user-story: Service Integration
x-controller: UserController
security:
- BearerAuth: []
parameters:
- name: userId
in: path
required: true
description: 사용자 ID
schema:
type: integer
format: int64
example: 123
responses:
'200':
description: 매장정보 조회 성공
content:
application/json:
schema:
$ref: '#/components/schemas/StoreDetailResponse'
examples:
success:
summary: 매장정보 조회 성공 응답
value:
userId: 123
storeId: 456
storeName: 맛있는집
industry: 음식점
address: 서울시 강남구 테헤란로 123
businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
'401':
description: 인증 실패
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
unauthorized:
summary: 인증 실패
value:
code: AUTH_002
message: 유효하지 않은 토큰입니다
timestamp: 2025-10-22T10:30:00Z
'403':
description: 권한 없음 (내부 서비스만 접근 가능)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
forbidden:
summary: 권한 없음
value:
code: AUTH_003
message: 이 API는 내부 서비스만 접근 가능합니다
timestamp: 2025-10-22T10:30:00Z
'404':
description: 사용자 또는 매장을 찾을 수 없음
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
notFound:
summary: 사용자 또는 매장 없음
value:
code: USER_003
message: 사용자 또는 매장을 찾을 수 없습니다
timestamp: 2025-10-22T10:30:00Z
'500':
description: 서버 오류
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT Bearer 토큰 인증
**형식:** Authorization: Bearer {JWT_TOKEN}
**토큰 만료:** 7일
**Claims:**
- userId: 사용자 ID
- role: 사용자 역할 (OWNER)
- iat: 발급 시각
- exp: 만료 시각
schemas:
RegisterRequest:
type: object
required:
- name
- phoneNumber
- email
- password
- storeName
- industry
- address
properties:
name:
type: string
minLength: 2
maxLength: 50
description: 사용자 이름 (2자 이상, 한글/영문)
example: 홍길동
phoneNumber:
type: string
pattern: '^010\d{8}$'
description: 휴대폰 번호 (010XXXXXXXX)
example: "01012345678"
email:
type: string
format: email
maxLength: 100
description: 이메일 주소
example: hong@example.com
password:
type: string
minLength: 8
maxLength: 100
description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
example: "Password123!"
storeName:
type: string
minLength: 2
maxLength: 100
description: 매장명
example: 맛있는집
industry:
type: string
maxLength: 50
description: 업종 (예 음식점, 카페, 소매점 등)
example: 음식점
address:
type: string
minLength: 5
maxLength: 200
description: 매장 주소
example: 서울시 강남구 테헤란로 123
businessHours:
type: string
maxLength: 200
description: 영업시간 (선택 사항)
example: "월-금 11:00-22:00, 토-일 12:00-21:00"
RegisterResponse:
type: object
required:
- token
- userId
- userName
- storeId
- storeName
properties:
token:
type: string
description: JWT 토큰 (7일 만료)
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
userId:
type: integer
format: int64
description: 사용자 ID
example: 123
userName:
type: string
description: 사용자 이름
example: 홍길동
storeId:
type: integer
format: int64
description: 매장 ID
example: 456
storeName:
type: string
description: 매장명
example: 맛있는집
LoginRequest:
type: object
required:
- phoneNumber
- password
properties:
phoneNumber:
type: string
pattern: '^010\d{8}$'
description: 휴대폰 번호
example: "01012345678"
password:
type: string
minLength: 8
description: 비밀번호
example: "Password123!"
LoginResponse:
type: object
required:
- token
- userId
- userName
- role
- email
properties:
token:
type: string
description: JWT 토큰 (7일 만료)
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
userId:
type: integer
format: int64
description: 사용자 ID
example: 123
userName:
type: string
description: 사용자 이름
example: 홍길동
role:
type: string
enum: [OWNER, ADMIN]
description: 사용자 역할
example: OWNER
email:
type: string
format: email
description: 이메일 주소
example: hong@example.com
LogoutResponse:
type: object
required:
- success
- message
properties:
success:
type: boolean
description: 로그아웃 성공 여부
example: true
message:
type: string
description: 응답 메시지
example: 안전하게 로그아웃되었습니다
ProfileResponse:
type: object
required:
- userId
- userName
- phoneNumber
- email
- role
- storeId
- storeName
- industry
- address
properties:
userId:
type: integer
format: int64
description: 사용자 ID
example: 123
userName:
type: string
description: 사용자 이름
example: 홍길동
phoneNumber:
type: string
description: 휴대폰 번호
example: "01012345678"
email:
type: string
format: email
description: 이메일 주소
example: hong@example.com
role:
type: string
enum: [OWNER, ADMIN]
description: 사용자 역할
example: OWNER
storeId:
type: integer
format: int64
description: 매장 ID
example: 456
storeName:
type: string
description: 매장명
example: 맛있는집
industry:
type: string
description: 업종
example: 음식점
address:
type: string
description: 매장 주소
example: 서울시 강남구 테헤란로 123
businessHours:
type: string
description: 영업시간
example: "월-금 11:00-22:00, 토-일 12:00-21:00"
createdAt:
type: string
format: date-time
description: 가입 일시
example: 2025-09-01T10:00:00Z
lastLoginAt:
type: string
format: date-time
description: 최종 로그인 일시
example: 2025-10-22T09:00:00Z
UpdateProfileRequest:
type: object
properties:
name:
type: string
minLength: 2
maxLength: 50
description: 사용자 이름 (선택 사항)
example: 홍길동
phoneNumber:
type: string
pattern: '^010\d{8}$'
description: 휴대폰 번호 (선택 사항, 향후 재인증 필요)
example: "01012345678"
email:
type: string
format: email
maxLength: 100
description: 이메일 주소 (선택 사항)
example: hong.new@example.com
storeName:
type: string
minLength: 2
maxLength: 100
description: 매장명 (선택 사항)
example: 맛있는집 (리뉴얼)
industry:
type: string
maxLength: 50
description: 업종 (선택 사항)
example: 퓨전음식점
address:
type: string
minLength: 5
maxLength: 200
description: 매장 주소 (선택 사항)
example: 서울시 강남구 테헤란로 456
businessHours:
type: string
maxLength: 200
description: 영업시간 (선택 사항)
example: "매일 11:00-23:00"
UpdateProfileResponse:
type: object
required:
- userId
- userName
- email
- storeId
- storeName
properties:
userId:
type: integer
format: int64
description: 사용자 ID
example: 123
userName:
type: string
description: 사용자 이름
example: 홍길동
email:
type: string
format: email
description: 이메일 주소
example: hong.new@example.com
storeId:
type: integer
format: int64
description: 매장 ID
example: 456
storeName:
type: string
description: 매장명
example: 맛있는집 (리뉴얼)
ChangePasswordRequest:
type: object
required:
- currentPassword
- newPassword
properties:
currentPassword:
type: string
minLength: 8
description: 현재 비밀번호
example: "Password123!"
newPassword:
type: string
minLength: 8
maxLength: 100
description: 새 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
example: "NewSecurePass456!"
ChangePasswordResponse:
type: object
required:
- success
- message
properties:
success:
type: boolean
description: 비밀번호 변경 성공 여부
example: true
message:
type: string
description: 응답 메시지
example: 비밀번호가 성공적으로 변경되었습니다
StoreDetailResponse:
type: object
required:
- userId
- storeId
- storeName
- industry
- address
properties:
userId:
type: integer
format: int64
description: 사용자 ID
example: 123
storeId:
type: integer
format: int64
description: 매장 ID
example: 456
storeName:
type: string
description: 매장명
example: 맛있는집
industry:
type: string
description: 업종
example: 음식점
address:
type: string
description: 매장 주소
example: 서울시 강남구 테헤란로 123
businessHours:
type: string
description: 영업시간
example: "월-금 11:00-22:00, 토-일 12:00-21:00"
ErrorResponse:
type: object
required:
- code
- message
- timestamp
properties:
code:
type: string
description: 에러 코드
example: USER_001
enum:
- USER_001 # 중복 사용자
- USER_003 # 사용자 없음
- USER_004 # 현재 비밀번호 불일치
- USER_005 # 동시성 충돌
- AUTH_001 # 인증 실패
- AUTH_002 # 유효하지 않은 토큰
- AUTH_003 # 권한 없음 (내부 서비스만 접근)
- VALIDATION_ERROR # 입력 검증 오류
message:
type: string
description: 에러 메시지
example: 이미 가입된 전화번호입니다
timestamp:
type: string
format: date-time
description: 에러 발생 시각
example: 2025-10-22T10:30:00Z
details:
type: array
description: 상세 에러 정보 (선택 사항)
items:
type: string
example: ["필드명: 필수 항목입니다"]