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: email: hong@example.com 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: - email - password properties: email: type: string format: email maxLength: 100 description: 이메일 주소 example: hong@example.com 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: ["필드명: 필수 항목입니다"]