This commit is contained in:
ondal 2025-02-13 18:42:46 +09:00
parent ba3405bff3
commit d7ca5994b4
48 changed files with 1071 additions and 7 deletions

Binary file not shown.

Binary file not shown.

1
.idea/compiler.xml generated
View File

@ -2,6 +2,7 @@
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">

View File

@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="FormSpellChecking" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +1,6 @@
plugins {
id 'org.springframework.boot' version '3.4.0' apply false
id 'io.spring.dependency-management' version '1.1.6' apply false
//id 'io.spring.dependency-management' version '1.1.6' apply false
id 'java'
}
@ -50,6 +50,8 @@ configure(subprojects.findAll { !it.name.endsWith('-biz') && it.name != 'common'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// AOP:
implementation 'org.springframework.boot:spring-boot-starter-aop'
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
}

View File

@ -0,0 +1,18 @@
서비스명|구독추천
마이크로서비스 이름|SubRecommend
유저스토리 ID|RSS-005
유저스토리 제목|추천 구독 카테고리
Controller 이름|RecommendController
API 목적|사용자의 지출 패턴을 분석하여 추천 구독 카테고리 제공
API Method|GET
API 그룹 Path|/api/recommend
API Path|/categories
Path <변수유형> <변수명>|
Query Key|userId
Query <변수유형> <변수명>|String userId
Request DTO 이름|
Request DTO 배열 여부|
Request DTO 구조|
Response DTO 이름|RecommendCategoryDTO
Response DTO 배열 여부|No
Response DTO 구조|String categoryName; LocalDate baseDate; String spendingCategory; Long totalSpending

View File

@ -0,0 +1,37 @@
!theme mono
title Recommendation Service - 데이터 모델
' Style configurations
skinparam linetype ortho
hide circle
entity "Spending_History" as spending {
* id: bigint <<PK>>
--
userId: varchar(50)
category: varchar(50)
amount: decimal(15,2)
spendingDate: date
createdAt: timestamp
}
entity "Recommended_Categories" as recommend {
* id: bigint <<PK>>
--
spendingCategory: varchar(50)
recommendCategory: varchar(50)
createdAt: timestamp
}
note right of spending
사용자별 지출 이력 관리
- 금액
- 카테고리
- 지출일자
end note
note right of recommend
지출-구독 카테고리 매핑
- 지출 카테고리
- 추천 구독 카테고리
end note

View File

@ -0,0 +1,54 @@
!theme mono
title 구독추천 서비스 - 내부 시퀀스 다이어그램
actor Client
participant "추천 컨트롤러\n(RecommendController)" as Controller
participant "추천 서비스\n(RecommendService)" as Service
participant "지출분석 서비스\n(SpendingAnalyzer)" as Analyzer
database "구독추천 DB" as RecommendDB
' 지출 기반 추천 카테고리 조회
Client -> Controller: GET /api/recommend/categories\n[지출 기반 추천 카테고리 조회]
activate Controller
Controller -> Service: getRecommendedCategory(userId)
activate Service
' 사용자 지출 패턴 분석
Service -> Analyzer: analyzeSpending(spendings)
activate Analyzer
Analyzer -> RecommendDB: findSpendingsByUserIdAndDateAfter(userId, startDate)
RecommendDB --> Analyzer: List<SpendingEntity>
Analyzer -> Analyzer: calculateTotalByCategory()
Analyzer -> Analyzer: findTopCategory()
Analyzer --> Service: SpendingCategory
deactivate Analyzer
' 추천 카테고리 매핑
Service -> RecommendDB: findBySpendingCategory(topSpendingCategory)
RecommendDB --> Service: RecommendedCategory
' 추천 결과 반환
Service --> Controller: RecommendCategoryDTO
Controller --> Client: HTTP Response\n(recommendCategory, spendingCategory, baseDate)
deactivate Service
deactivate Controller
note right of Controller
1. 요청 파라미터 검증
2. 서비스 계층 호출
3. 응답 변환
end note
note right of Service
1. 지출 패턴 분석 요청
2. 추천 카테고리 매핑
3. 추천 결과 생성
end note
note right of Analyzer
1. 최근 1개월 지출 데이터 조회
2. 카테고리별 지출 합산
3. 최고 지출 카테고리 도출
end note

View File

@ -0,0 +1,81 @@
!theme mono
title Recommendation Service - Class Diagram
package "com.unicorn.lifesub.recommend" {
package "domain" {
class SpendingCategory {
-category: String
-totalAmount: Long
}
class RecommendedCategory {
-spendingCategory: String
-recommendCategory: String
-baseDate: LocalDate
}
}
package "service" {
interface RecommendService {
+getRecommendedCategory(userId: String): RecommendCategoryDTO
}
class RecommendServiceImpl {
-recommendRepository: RecommendRepository
-spendingRepository: SpendingRepository
-spendingAnalyzer: SpendingAnalyzer
+getRecommendedCategory(userId: String): RecommendCategoryDTO
}
class SpendingAnalyzer {
+analyzeSpending(spendings: List<SpendingEntity>): SpendingCategory
-calculateTotalByCategory(spendings: List<SpendingEntity>): Map<String, Long>
-findTopCategory(totals: Map<String, Long>): SpendingCategory
}
}
package "controller" {
class RecommendController {
-recommendService: RecommendService
+getRecommendedCategory(userId: String): ResponseEntity<ApiResponse<RecommendCategoryDTO>>
}
}
package "dto" {
class RecommendCategoryDTO {
-categoryName: String
-baseDate: LocalDate
-spendingCategory: String
-totalSpending: Long
}
}
package "repository" {
package "jpa" {
interface SpendingRepository {
+findSpendingsByUserIdAndDateAfter(userId: String, startDate: LocalDate): List<SpendingEntity>
}
interface RecommendRepository {
+findBySpendingCategory(category: String): Optional<RecommendedCategoryEntity>
}
}
package "entity" {
class SpendingEntity {
-id: Long
-userId: String
-category: String
-amount: Long
-spendingDate: LocalDate
}
class RecommendedCategoryEntity {
-id: Long
-spendingCategory: String
-recommendCategory: String
+toDomain(): RecommendedCategory
}
}
}
}

78
design/논리아키텍처 Normal file
View File

@ -0,0 +1,78 @@
!theme mono
title 구독관리 서비스 - 논리 아키텍처
' Components
package "클라이언트 계층" {
[모바일/웹 앱] as App
}
package "회원 서비스" {
[회원 컨트롤러] as MemberController
[회원 서비스] as MemberService
[JWT 토큰 제공자] as JwtTokenProvider
database "회원 DB" as MemberDB
note right of MemberService
1. 로그인/로그아웃 처리
2. JWT 토큰 생성/검증
end note
}
package "마이구독 서비스" {
[마이구독 컨트롤러] as MySubController
[카테고리 컨트롤러] as CategoryController
[서비스 컨트롤러] as ServiceController
[마이구독 서비스] as MySubService
database "마이구독 DB" as MySubDB {
[사용자별 구독 정보]
[구독 서비스 정보]
[카테고리 정보]
}
note right of MySubService
4. 총 구독료 계산
5. 나의 구독 목록 관리
8. 구독 상세 정보 제공
9. 구독 신청 처리
10. 구독 취소 처리
11. 구독 카테고리 관리
12. 카테고리별 구독 서비스 제공
end note
}
package "구독추천 서비스" {
[구독추천 컨트롤러] as RecommendController
[구독추천 서비스] as RecommendService
[지출분석 서비스] as SpendingAnalyzer
database "구독추천 DB" as RecommendDB {
[지출 이력]
[추천 카테고리]
}
note right of RecommendService
6. 지출 카테고리 기반 구독 추천
- 사용자의 지출 패턴 분석
- 최적 구독 카테고리 추천
end note
}
' Relationships
App --> MemberController
App --> MySubController
App --> CategoryController
App --> ServiceController
App --> RecommendController
MemberController --> MemberService
MemberService --> JwtTokenProvider
MemberService --> MemberDB
MySubController --> MySubService
CategoryController --> MySubService
ServiceController --> MySubService
MySubService --> MySubDB
RecommendController --> RecommendService
RecommendService --> SpendingAnalyzer
RecommendService --> RecommendDB

View File

@ -0,0 +1,18 @@
서비스명|마이구독|마이구독|마이구독|마이구독|마이구독|마이구독|마이구독
마이크로서비스 이름|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription|MySubscription
유저스토리 ID|MSS-005|MSS-010|MSS-020|MSS-025|MSS-030|MSS-035|MSS-040
유저스토리 제목|총 구독료 표시|나의 구독 목록|구독상세|구독하기|구독취소|구독 카테고리 표시|구독서비스 목록
Controller 이름|MySubController|MySubController|ServiceController|ServiceController|ServiceController|CategoryController|CategoryController
API 목적|총 구독료 조회|구독 목록 조회|구독 상세 조회|구독 신청|구독 취소|전체 카테고리 목록 조회|카테고리별 서비스 목록 조회
API Method|GET|GET|GET|POST|DELETE|GET|GET
API 그룹 Path|/api/mysub|/api/mysub|/api/mysub/services|/api/mysub/services|/api/mysub/services|/api/mysub|/api/mysub
API Path|/total-fee|/list|/{subscriptionId}|/{subscriptionId}/subscribe|/{subscriptionId}|/categories|/services
Path <변수유형> <변수명>|||Long subscriptionId|Long subscriptionId|Long subscriptionId||
Query Key|userId|userId||userId|||categoryId
Query <변수유형> <변수명>|String userId|String userId||String userId|||String categoryId
Request DTO 이름|||||||
Request DTO 배열 여부|||||||
Request DTO 구조|||||||
Response DTO 이름|TotalFeeResponse|MySubResponse|SubDetailResponse||null|CategoryResponse|ServiceListResponse
Response DTO 배열 여부|No|Yes|No||No|Yes|Yes
Response DTO 구조|Long totalFee; String feeLevel|String serviceName; String logoUrl|String serviceName; String logoUrl; String category; String description; int price; int maxSharedUsers||null|String categoryId; String categoryName|String serviceId; String serviceName; String description; int price; String logoUrl

View File

@ -0,0 +1,52 @@
!theme mono
title MySubscription Service - 데이터 모델
' Style configurations
skinparam linetype ortho
hide circle
entity "My_Subscriptions" as my_subs {
* id: bigint <<PK>>
--
userId: varchar(50)
subscriptionId: bigint <<FK>>
createdAt: timestamp
updatedAt: timestamp
}
entity "Subscriptions" as subs {
* id: bigint <<PK>>
--
name: varchar(100)
description: text
category: varchar(50)
price: decimal(15,2)
maxSharedUsers: integer
logoUrl: varchar(255)
createdAt: timestamp
updatedAt: timestamp
}
entity "Categories" as categories {
* categoryId: varchar(50) <<PK>>
--
name: varchar(100)
createdAt: timestamp
updatedAt: timestamp
}
' Relationships
my_subs }o--|| subs : subscriptionId
subs }o--|| categories : category-categoryId
note right of my_subs
사용자별 구독 정보 관리
end note
note right of subs
구독 서비스 정보 관리
end note
note right of categories
구독 카테고리 관리
end note

View File

@ -0,0 +1,101 @@
!theme mono
title 마이구독 서비스 - 내부 시퀀스 다이어그램
actor Client
participant "마이구독 컨트롤러\n(MySubController)" as MySubController
participant "서비스 컨트롤러\n(ServiceController)" as ServiceController
participant "카테고리 컨트롤러\n(CategoryController)" as CategoryController
participant "마이구독 서비스\n(MySubService)" as Service
database "마이구독 DB" as MySubDB
database "구독서비스 DB" as SubServiceDB
' 총 구독료 조회
Client -> MySubController: GET /api/mysub/total-fee\n[총 구독료 조회]
activate MySubController
MySubController -> Service: getTotalFee(userId)
activate Service
Service -> MySubDB: findMySubscriptions(userId)
MySubDB --> Service: List<MySubscription>
Service -> SubServiceDB: findSubscriptionsByIds(subIds)
SubServiceDB --> Service: List<Subscription>
Service --> MySubController: TotalFeeResponse
MySubController --> Client: HTTP Response\n(total fee, level)
deactivate Service
deactivate MySubController
' 나의 구독 목록 조회
Client -> MySubController: GET /api/mysub/list\n[구독 목록 조회]
activate MySubController
MySubController -> Service: getMySubscriptions(userId)
activate Service
Service -> MySubDB: findMySubscriptions(userId)
MySubDB --> Service: List<MySubscription>
Service --> MySubController: List<MySubResponse>
MySubController --> Client: HTTP Response\n(subscription list)
deactivate Service
deactivate MySubController
' 구독 상세 조회
Client -> ServiceController: GET /api/mysub/services/{subscriptionId}\n[구독 상세 조회]
activate ServiceController
ServiceController -> Service: getSubscriptionDetail(subscriptionId)
activate Service
Service -> SubServiceDB: findById(subscriptionId)
SubServiceDB --> Service: Subscription
Service --> ServiceController: SubDetailResponse
ServiceController --> Client: HTTP Response\n(subscription detail)
deactivate Service
deactivate ServiceController
' 구독 신청
Client -> ServiceController: POST /api/mysub/services/{subscriptionId}/subscribe\n[구독 신청]
activate ServiceController
ServiceController -> Service: subscribe(subscriptionId, userId)
activate Service
Service -> SubServiceDB: findById(subscriptionId)
Service -> MySubDB: save(mySubscription)
Service --> ServiceController: void
ServiceController --> Client: HTTP Response\n(success)
deactivate Service
deactivate ServiceController
' 구독 취소
Client -> ServiceController: DELETE /api/mysub/services/{subscriptionId}\n[구독 취소]
activate ServiceController
ServiceController -> Service: cancel(subscriptionId)
activate Service
Service -> MySubDB: delete(subscriptionId)
Service --> ServiceController: void
ServiceController --> Client: HTTP Response\n(success)
deactivate Service
deactivate ServiceController
' 카테고리 목록 조회
Client -> CategoryController: GET /api/mysub/categories\n[카테고리 목록 조회]
activate CategoryController
CategoryController -> Service: getAllCategories()
activate Service
Service -> SubServiceDB: findAllCategories()
SubServiceDB --> Service: List<Category>
Service --> CategoryController: List<CategoryResponse>
CategoryController --> Client: HTTP Response\n(category list)
deactivate Service
deactivate CategoryController
' 카테고리별 서비스 목록 조회
Client -> CategoryController: GET /api/mysub/services\n[카테고리별 서비스 목록 조회]
activate CategoryController
CategoryController -> Service: getServicesByCategory(categoryId)
activate Service
Service -> SubServiceDB: findByCategory(categoryId)
SubServiceDB --> Service: List<Subscription>
Service --> CategoryController: List<ServiceListResponse>
CategoryController --> Client: HTTP Response\n(service list)
deactivate Service
deactivate CategoryController
note right of Service
1. 비즈니스 로직 처리
2. 구독 관리
3. 카테고리 관리
end note

View File

@ -0,0 +1,260 @@
!theme mono
title MySubscription Service - Clean Architecture Class Diagram
' BIZ Layer
package "com.unicorn.lifesub.mysub.biz" {
package "usecase" {
package "in" {
interface TotalFeeUseCase {
+getTotalFee(userId: String): TotalFeeResponse
}
interface MySubscriptionsUseCase {
+getMySubscriptions(userId: String): List<MySubResponse>
}
interface SubscriptionDetailUseCase {
+getSubscriptionDetail(subscriptionId: Long): SubDetailResponse
}
interface SubscribeUseCase {
+subscribe(subscriptionId: Long, userId: String): void
}
interface CancelSubscriptionUseCase {
+cancel(subscriptionId: Long): void
}
interface CategoryUseCase {
+getAllCategories(): List<CategoryResponse>
+getServicesByCategory(categoryId: String): List<ServiceListResponse>
}
}
package "out" {
interface MySubscriptionReader {
+findByUserId(userId: String): List<MySubscription>
+findById(id: Long): Optional<MySubscription>
}
interface MySubscriptionWriter {
+save(userId: String, subscriptionId: Long): MySubscription
+delete(id: Long): void
}
interface SubscriptionReader {
+findById(id: Long): Optional<Subscription>
+findByCategory(category: String): List<Subscription>
+findAllCategories(): List<Category>
}
}
}
package "domain" {
class MySubscription {
-id: Long
-userId: String
-subscription: Subscription
+getPrice(): int
}
class Subscription {
-id: Long
-name: String
-description: String
-category: String
-price: int
-maxSharedUsers: int
-logoUrl: String
}
class Category {
-categoryId: String
-name: String
}
}
package "service" {
class MySubscriptionService {
-mySubscriptionReader: MySubscriptionReader
-mySubscriptionWriter: MySubscriptionWriter
-subscriptionReader: SubscriptionReader
-collectorThreshold: long
-addictThreshold: long
+getTotalFee(userId: String): TotalFeeResponse
+getMySubscriptions(userId: String): List<MySubResponse>
+getSubscriptionDetail(subscriptionId: Long): SubDetailResponse
+subscribe(subscriptionId: Long, userId: String): void
+cancel(subscriptionId: Long): void
+getAllCategories(): List<CategoryResponse>
+getServicesByCategory(categoryId: String): List<ServiceListResponse>
-calculateFeeLevel(totalFee: long): String
}
enum FeeLevel {
LIKFER
COLLECTOR
ADDICT
}
}
package "dto" {
class TotalFeeResponse {
-totalFee: Long
-feeLevel: String
}
class MySubResponse {
-id: Long
-serviceName: String
-logoUrl: String
}
class SubDetailResponse {
-serviceName: String
-logoUrl: String
-category: String
-description: String
-price: int
-maxSharedUsers: int
}
class CategoryResponse {
-categoryId: String
-categoryName: String
}
class ServiceListResponse {
-serviceId: String
-serviceName: String
-description: String
-price: int
-logoUrl: String
}
}
}
' INFRA Layer
package "com.unicorn.lifesub.mysub.infra" {
package "controller" {
class MySubController {
-totalFeeUseCase: TotalFeeUseCase
-mySubscriptionsUseCase: MySubscriptionsUseCase
+getTotalFee(userId: String): ResponseEntity<ApiResponse<TotalFeeResponse>>
+getMySubscriptions(userId: String): ResponseEntity<ApiResponse<List<MySubResponse>>>
}
class ServiceController {
-subscriptionDetailUseCase: SubscriptionDetailUseCase
-subscribeUseCase: SubscribeUseCase
-cancelSubscriptionUseCase: CancelSubscriptionUseCase
+getSubscriptionDetail(subscriptionId: Long): ResponseEntity<ApiResponse<SubDetailResponse>>
+subscribe(subscriptionId: Long, userId: String): ResponseEntity<ApiResponse<Void>>
+cancel(subscriptionId: Long): ResponseEntity<ApiResponse<Void>>
}
class CategoryController {
-categoryUseCase: CategoryUseCase
+getAllCategories(): ResponseEntity<ApiResponse<List<CategoryResponse>>>
+getServicesByCategory(categoryId: String): ResponseEntity<ApiResponse<List<ServiceListResponse>>>
}
}
package "gateway" {
class MySubscriptionGateway implements MySubscriptionReader, MySubscriptionWriter {
-mySubscriptionRepository: MySubscriptionJpaRepository
-subscriptionRepository: SubscriptionJpaRepository
}
class SubscriptionGateway implements SubscriptionReader {
-subscriptionRepository: SubscriptionJpaRepository
-categoryRepository: CategoryJpaRepository
}
package "repository" {
interface MySubscriptionJpaRepository {
+findByUserId(userId: String): List<MySubscriptionEntity>
+findBySubscription_Id(subscriptionId: Long): Optional<MySubscriptionEntity>
}
interface SubscriptionJpaRepository {
+findById(id: Long): Optional<SubscriptionEntity>
+findByCategory(category: String): List<SubscriptionEntity>
}
interface CategoryJpaRepository {
+findAll(): List<CategoryEntity>
}
}
package "entity" {
class MySubscriptionEntity {
-id: Long
-userId: String
-subscription: SubscriptionEntity
+toDomain(): MySubscription
}
class SubscriptionEntity {
-id: Long
-name: String
-description: String
-category: String
-price: int
-maxSharedUsers: int
-logoUrl: String
+toDomain(): Subscription
}
class CategoryEntity {
-categoryId: String
-name: String
+toDomain(): Category
}
}
}
package "config" {
class SecurityConfig {
-jwtTokenProvider: JwtTokenProvider
+securityFilterChain(http: HttpSecurity): SecurityFilterChain
+corsConfigurationSource(): CorsConfigurationSource
}
class SwaggerConfig {
+openAPI(): OpenAPI
}
class DataLoader {
-categoryRepository: CategoryJpaRepository
-subscriptionRepository: SubscriptionJpaRepository
+run(): void
}
}
}
' Relationships
MySubscriptionService ..|> TotalFeeUseCase
MySubscriptionService ..|> MySubscriptionsUseCase
MySubscriptionService ..|> SubscriptionDetailUseCase
MySubscriptionService ..|> SubscribeUseCase
MySubscriptionService ..|> CancelSubscriptionUseCase
MySubscriptionService ..|> CategoryUseCase
MySubController --> TotalFeeUseCase
MySubController --> MySubscriptionsUseCase
ServiceController --> SubscriptionDetailUseCase
ServiceController --> SubscribeUseCase
ServiceController --> CancelSubscriptionUseCase
CategoryController --> CategoryUseCase
MySubscriptionGateway --> MySubscriptionJpaRepository
MySubscriptionGateway --> SubscriptionJpaRepository
SubscriptionGateway --> SubscriptionJpaRepository
SubscriptionGateway --> CategoryJpaRepository
MySubscriptionEntity ..> MySubscription : converts to
SubscriptionEntity ..> Subscription : converts to
CategoryEntity ..> Category : converts to
MySubscription --> Subscription

74
design/물리아키텍처 Normal file
View File

@ -0,0 +1,74 @@
!theme mono
title ResourceGroup - 물리아키텍처
' Azure Resource Group
rectangle "ResourceGroup" {
' Virtual Network
rectangle "VirtualNetwork" {
' AKS Cluster
rectangle "AKSCluster" {
rectangle "SystemNodePool" {
[IngressController] as ingress
}
rectangle "UserNodePool" {
rectangle "MemberServicePod" {
[회원서비스] as memberservice
}
rectangle "MySubscriptionServicePod" {
[마이구독서비스] as mysubservice
}
rectangle "RecommendServicePod" {
[구독추천서비스] as recommendservice
}
}
}
}
' Managed Databases
database "AzureDatabasePostgreSQL" {
database "MemberDB" as memberdb {
[Members]
}
database "MySubscriptionDB" as mysubdb {
[MySubscriptions]
[Subscriptions]
[Categories]
}
database "RecommendDB" as recommenddb {
[SpendingHistory]
[RecommendedCategories]
}
}
}
' External Actors
actor "Client" as client
actor "Developer" as developer
' Network Flow
client --> ingress
ingress --> memberservice
ingress --> mysubservice
ingress --> recommendservice
' Database Connections
memberservice --> memberdb
mysubservice --> mysubdb
recommendservice --> recommenddb
' Development Access
developer --> ingress : "kubectl"
' Legend
legend right
| Component | Description |
|---|---|
| Infrastructure | AKSCluster |
| Services | Member, MySubscription, Recommend |
| Databases | PostgreSQL(Member, MySubscription, Recommend) |
endlegend

View File

@ -0,0 +1,42 @@
!theme mono
title 구독관리 서비스 - 외부 시퀀스 다이어그램
actor Client
participant "회원 서비스" as MemberService
participant "마이구독 서비스" as MySubService
participant "구독추천 서비스" as RecommendService
' 회원 서비스 호출
Client -> MemberService: POST /api/auth/login\n[로그인]
Client -> MemberService: POST /api/auth/logout\n[로그아웃]
' 마이구독 서비스 호출
Client -> MySubService: GET /api/mysub/total-fee\n[총 구독료 조회]
Client -> MySubService: GET /api/mysub/list\n[구독 목록 조회]
Client -> MySubService: GET /api/mysub/categories\n[전체 카테고리 목록 조회]
Client -> MySubService: GET /api/mysub/services\n[카테고리별 서비스 목록 조회]
Client -> MySubService: GET /api/mysub/services/{subscriptionId}\n[구독 상세 조회]
Client -> MySubService: POST /api/mysub/services/{subscriptionId}/subscribe\n[구독 신청]
Client -> MySubService: DELETE /api/mysub/services/{subscriptionId}\n[구독 취소]
' 구독추천 서비스 호출
Client -> RecommendService: GET /api/recommend/categories\n[지출 기반 추천 카테고리 조회]
note right of MemberService
인증/인가 처리
- JWT 기반 토큰 인증
- Role 기반 권한 관리
end note
note right of MySubService
구독 서비스 관리
- 구독 정보 관리
- 카테고리별 서비스 제공
- 구독료 계산
end note
note right of RecommendService
지출 분석 기반 추천
- 사용자별 지출 패턴 분석
- 최적 구독 카테고리 추천
end note

View File

@ -0,0 +1,18 @@
서비스명|회원|회원
마이크로서비스 이름|Member|Member
유저스토리 ID|USR-005|USR-015
유저스토리 제목|로그인|로그아웃
Controller 이름|MemberController|MemberController
API 목적|사용자 로그인|로그아웃
API Method|POST|POST
API 그룹 Path|/api/auth|/api/auth
API Path|/login|/logout
Path <변수유형> <변수명>||
Query Key||
Query <변수유형> <변수명>||
Request DTO 이름|LoginRequest|LogoutRequest
Request DTO 배열 여부|No|No
Request DTO 구조|String userId; String password|String userId
Response DTO 이름|JwtTokenDTO|LogoutResponse
Response DTO 배열 여부|No|No
Response DTO 구조|String accessToken; String refreshToken|String message

View File

@ -0,0 +1,17 @@
!theme mono
title Member Service - 데이터 모델
entity "Members" as members {
* userId: varchar(50) <<PK>>
--
userName: varchar(100)
password: varchar(255)
roles: varchar(255)
createdAt: timestamp
updatedAt: timestamp
}
note right of members
roles는 ARRAY 또는 JSON 타입으로
['USER', 'ADMIN'] 형태로 저장
end note

View File

@ -0,0 +1,72 @@
!theme mono
title 회원 서비스 - 내부 시퀀스 다이어그램
actor Client
participant "회원 컨트롤러\n(MemberController)" as Controller
participant "회원 서비스\n(MemberService)" as Service
participant "JWT 토큰 제공자\n(JwtTokenProvider)" as TokenProvider
participant "비밀번호 인코더\n(PasswordEncoder)" as PwEncoder
database "회원 DB" as DB
' 로그인 flow
Client -> Controller: POST /api/auth/login\n[로그인]
activate Controller
Controller -> Service: login(LoginRequest)
activate Service
Service -> DB: findByUserId(userId)
activate DB
DB --> Service: Member
deactivate DB
Service -> PwEncoder: matches(rawPassword, encodedPassword)
activate PwEncoder
PwEncoder --> Service: matched result
deactivate PwEncoder
alt 인증 성공
Service -> TokenProvider: createToken(member)
activate TokenProvider
TokenProvider --> Service: access/refresh tokens
deactivate TokenProvider
Service --> Controller: TokenResponse
else 인증 실패
Service --> Controller: throw InvalidCredentialsException
end
Controller --> Client: HTTP Response\n(tokens or error)
deactivate Service
deactivate Controller
' 로그아웃 flow
Client -> Controller: POST /api/auth/logout\n[로그아웃]
activate Controller
Controller -> Service: logout(LogoutRequest)
activate Service
Service --> Controller: LogoutResponse
Controller --> Client: HTTP Response\n(success message)
deactivate Service
deactivate Controller
note right of Controller
1. 요청 유효성 검증
2. 서비스 계층 호출
3. 응답 변환 및 반환
end note
note right of Service
1. 비즈니스 로직 처리
2. 사용자 인증
3. 토큰 관리
end note
note right of TokenProvider
1. JWT 토큰 생성
2. 토큰 검증
3. 토큰 무효화
end note

View File

@ -0,0 +1,89 @@
!theme mono
title Member Service - Class Diagram
package "com.unicorn.lifesub.member" {
package "domain" {
class Member {
-userId: String
-userName: String
-password: String
-roles: Set<String>
+Member(userId: String, userName: String, password: String, roles: Set<String>)
}
}
package "service" {
interface MemberService {
+login(request: LoginRequest): JwtTokenDTO
+logout(request: LogoutRequest): LogoutResponse
}
class MemberServiceImpl {
-memberRepository: MemberRepository
-passwordEncoder: PasswordEncoder
-jwtTokenProvider: JwtTokenProvider
+login(request: LoginRequest): JwtTokenDTO
+logout(request: LogoutRequest): LogoutResponse
}
}
package "controller" {
class MemberController {
-memberService: MemberService
+login(request: LoginRequest): ResponseEntity<ApiResponse<JwtTokenDTO>>
+logout(request: LogoutRequest): ResponseEntity<ApiResponse<LogoutResponse>>
}
}
package "dto" {
class LoginRequest {
-userId: String
-password: String
}
class LogoutRequest {
-userId: String
}
class LogoutResponse {
-message: String
}
}
package "repository" {
package "jpa" {
interface MemberRepository {
+findByUserId(userId: String): Optional<MemberEntity>
}
}
package "entity" {
class MemberEntity {
-userId: String
-userName: String
-password: String
-roles: Set<String>
+toDomain(): Member
+fromDomain(member: Member): MemberEntity
}
}
}
package "config" {
class SecurityConfig {
-jwtTokenProvider: JwtTokenProvider
+securityFilterChain(http: HttpSecurity): SecurityFilterChain
+corsConfigurationSource(): CorsConfigurationSource
+passwordEncoder(): PasswordEncoder
}
class JwtTokenProvider {
-algorithm: Algorithm
-accessTokenValidityInMilliseconds: long
-refreshTokenValidityInMilliseconds: long
+createToken(member: MemberEntity): JwtTokenDTO
+validateToken(token: String): boolean
+getAuthentication(token: String): Authentication
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.unicorn.lifesub.member.MemberApplication
Spring-Boot-Version: 3.4.0
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 18
Implementation-Title: member
Implementation-Version: 1.0.0

View File

@ -0,0 +1,2 @@
Manifest-Version: 1.0

View File

@ -24,9 +24,9 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@SuppressWarnings("unused")
@Configuration //Config 레이어의 클래스임을 나타내며 Bean클래스로 등록되어 실행시 자동으로 객체가 생성됨
@EnableWebSecurity //인증 처리 라이브러리인 Spring Security를 활성화함
@SuppressWarnings("unused") //unused 경고를 표시하지 않게 하는 어노테이션
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final CustomUserDetailsService customUserDetailsService;
@ -44,6 +44,16 @@ public class SecurityConfig {
return authConfig.getAuthenticationManager();
}
/*
아래와 같은 수행을
- CORS설정: 접근을 허용할 도메인, 메서드, 헤더값 등을 설정함
- csrf : Cross Site Request Forgery(인증된 세션을 사용하여 서버를 공격하는 행위) 비활성화
JWT 방식을 사용하므로 불필요함. 만약 CSRF까지 활성화하면 클라이언트는 CSRF토큰도 요청 헤더에 보내야
- authorizeHttpRequests: 인증이 필요없는 주소를 지정하고, 나머지는 인증이 안되어 있으면 접근을 금지시킴
swagger페이지와 로그인, 회원등록 페이지는 인증 안해도 접근하도록 설정함
- sessionManagement: 세션을 로컬에 저장하지 않도록
- userDetailsService: 사용자 인증을 처리할 class
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http

View File

@ -13,12 +13,16 @@ import java.util.Set;
@Entity
@Table(name = "members")
@Getter
@NoArgsConstructor
@NoArgsConstructor //JPA는 인자없는 기본 생성자를 대부분 요구하기 때문에 필요
public class MemberEntity extends BaseTimeEntity {
@Id
@Id ////PK(primary key)필드로 지정
@Column(name = "user_id", unique = true, nullable = false) //테이블 스키마 생성 필드명, 유일값
private String userId;
@Column(nullable = false)
private String userName;
@Column(nullable = false)
private String password;
@ElementCollection(fetch = FetchType.EAGER)

View File

@ -19,6 +19,7 @@ spring:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
# Secret key 만들기: openssl rand -base64 32
jwt:
secret-key: ${JWT_SECRET_KEY:8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ}

View File

@ -31,6 +31,15 @@ public class SecurityConfig {
this.jwtTokenProvider = jwtTokenProvider;
}
/*
아래와 같은 수행을
- CORS설정: 접근을 허용할 도메인, 메서드, 헤더값 등을 설정함
- csrf : Cross Site Request Forgery(인증된 세션을 사용하여 서버를 공격하는 행위) 비활성화
JWT 방식을 사용하므로 불필요함. 만약 CSRF까지 활성화하면 클라이언트는 CSRF토큰도 요청 헤더에 보내야
- authorizeHttpRequests: 인증이 필요없는 주소를 지정하고, 나머지는 인증이 안되어 있으면 접근을 금지시킴
swagger페이지와 로그인, 회원등록 페이지는 인증 안해도 접근하도록 설정함
- sessionManagement: 세션을 로컬에 저장하지 않도록
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http

View File

@ -19,6 +19,7 @@ spring:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
# Secret key 만들기: openssl rand -base64 32
jwt:
secret-key: ${JWT_SECRET_KEY:8O2HQ13etL2BWZvYOiWsJ5uWFoLi6NBUG8divYVoCgtHVvlk3dqRksMl16toztDUeBTSIuOOPvHIrYq11G2BwQ}