Skip to content

Latest commit

Β 

History

History
535 lines (439 loc) Β· 20.7 KB

File metadata and controls

535 lines (439 loc) Β· 20.7 KB

WorkingDead Backend - Codebase 뢄석

ν”„λ‘œμ νŠΈ κ°œμš”

When:D (웬디) - λ©€ν‹° ν”Œλž«νΌ 일정 쑰율 μ„œλΉ„μŠ€

νŒ€/그룹의 νšŒμ‹, λͺ¨μž„ 일정을 μ‘°μœ¨ν•˜κΈ° μœ„ν•œ νˆ¬ν‘œ μ‹œμŠ€ν…œμ„ μ œκ³΅ν•˜λ©°, Discord 봇과 μΉ΄μΉ΄μ˜€ν†‘ 챗봇 두 ν”Œλž«νΌμ„ 톡해 μžλ™ν™”λœ νˆ¬ν‘œ 생성 및 μ•Œλ¦Ό κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€.


기술 μŠ€νƒ

ꡬ뢄 기술
Language Java 21
Framework Spring Boot 3.5.7
Database PostgreSQL (AWS RDS)
ORM Spring Data JPA + Hibernate
Security Spring Security
Session Spring Session JDBC
Cache Redis
API Docs SpringDoc OpenAPI (Swagger)
Discord Bot JDA 5.0.0-beta.24
Kakao Chatbot Kakao i Open Builder (REST API)
Build Tool Gradle

ν”„λ‘œμ νŠΈ ꡬ쑰

src/main/java/com/workingdead/
β”œβ”€β”€ WorkingdeadApplication.java          # 메인 μ• ν”Œλ¦¬μΌ€μ΄μ…˜
β”‚
β”œβ”€β”€ config/                              # μ„€μ • 클래슀
β”‚   β”œβ”€β”€ SecurityConfig.java              # Spring Security μ„€μ •
β”‚   β”œβ”€β”€ CorsConfig.java                  # CORS μ„€μ •
β”‚   β”œβ”€β”€ OpenApiConfig.java               # Swagger μ„€μ •
β”‚   β”œβ”€β”€ DiscordBotConfig.java            # Discord JDA μ„€μ •
β”‚   └── KakaoConfig.java                 # Kakao API μ„€μ •
β”‚
β”œβ”€β”€ enum/
β”‚   └── Period.java                      # μ‹œκ°„λŒ€ enum (LUNCH/DINNER)
β”‚
β”œβ”€β”€ meet/                                # 핡심 도메인 (νˆ¬ν‘œ μ‹œμŠ€ν…œ)
β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”œβ”€β”€ VoteController.java          # νˆ¬ν‘œ CRUD API
β”‚   β”‚   β”œβ”€β”€ ParticipantController.java   # μ°Έμ—¬μž 관리 API
β”‚   β”‚   β”œβ”€β”€ VoteResultController.java    # νˆ¬ν‘œ κ²°κ³Ό 쑰회 API
β”‚   β”‚   └── VoteDateRangeController.java # λ‚ μ§œ λ²”μœ„ 쑰회 API
β”‚   β”‚
β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”œβ”€β”€ VoteService.java             # νˆ¬ν‘œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
β”‚   β”‚   β”œβ”€β”€ ParticipantService.java      # μ°Έμ—¬μž λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
β”‚   β”‚   β”œβ”€β”€ VoteResultService.java       # κ²°κ³Ό 집계 둜직
β”‚   β”‚   β”œβ”€β”€ VoteDateRangeService.java    # λ‚ μ§œ λ²”μœ„ 둜직
β”‚   β”‚   └── PriorityService.java         # μš°μ„ μˆœμœ„ 둜직
β”‚   β”‚
β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”œβ”€β”€ Vote.java                    # νˆ¬ν‘œ μ—”ν‹°ν‹°
β”‚   β”‚   β”œβ”€β”€ Participant.java             # μ°Έμ—¬μž μ—”ν‹°ν‹°
β”‚   β”‚   β”œβ”€β”€ ParticipantSelection.java    # 일정 선택 μ—”ν‹°ν‹°
β”‚   β”‚   └── PriorityPreference.java      # μš°μ„ μˆœμœ„ μ—”ν‹°ν‹°
β”‚   β”‚
β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”œβ”€β”€ VoteRepository.java
β”‚   β”‚   β”œβ”€β”€ ParticipantRepository.java
β”‚   β”‚   β”œβ”€β”€ ParticipantSelectionRepository.java
β”‚   β”‚   └── PriorityPreferenceRepository.java
β”‚   β”‚
β”‚   β”œβ”€β”€ dto/
β”‚   β”‚   β”œβ”€β”€ VoteDtos.java
β”‚   β”‚   β”œβ”€β”€ ParticipantDtos.java
β”‚   β”‚   β”œβ”€β”€ VoteResultDtos.java
β”‚   β”‚   β”œβ”€β”€ VoteDateRangeDtos.java
β”‚   β”‚   └── PriorityDtos.java
β”‚   β”‚
β”‚   └── application/
β”‚       └── VoteApplicationService.java  # μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλΉ„μŠ€
β”‚
└── chatbot/                             # 챗봇 λͺ¨λ“ˆ (λ©€ν‹° ν”Œλž«νΌ)
    β”‚
    β”œβ”€β”€ discord/                         # Discord 봇
    β”‚   β”œβ”€β”€ command/
    β”‚   β”‚   └── DiscordWendyCommand.java     # λ””μŠ€μ½”λ“œ λͺ…λ Ήμ–΄ ν•Έλ“€λŸ¬
    β”‚   β”œβ”€β”€ service/
    β”‚   β”‚   β”œβ”€β”€ DiscordWendyService.java     # 봇 μ„œλΉ„μŠ€ μΈν„°νŽ˜μ΄μŠ€
    β”‚   β”‚   β”œβ”€β”€ DiscordWendyServiceImpl.java # 봇 μ„œλΉ„μŠ€ κ΅¬ν˜„μ²΄
    β”‚   β”‚   └── DiscordWendyNotifier.java    # μ•Œλ¦Ό μ„œλΉ„μŠ€
    β”‚   β”œβ”€β”€ scheduler/
    β”‚   β”‚   └── DiscordWendyScheduler.java   # μŠ€μΌ€μ€„λŸ¬ (λ¦¬λ§ˆμΈλ“œ)
    β”‚   └── dto/
    β”‚       └── DiscordVoteResult.java       # νˆ¬ν‘œ κ²°κ³Ό DTO
    β”‚
    └── kakao/                           # Kakao 챗봇
        β”œβ”€β”€ controller/
        β”‚   └── KakaoSkillController.java    # Skill Server μ—”λ“œν¬μΈνŠΈ
        β”œβ”€β”€ service/
        β”‚   β”œβ”€β”€ KakaoWendyService.java       # μ„Έμ…˜ 관리 μ„œλΉ„μŠ€
        β”‚   └── KakaoNotifier.java           # μ•Œλ¦Ό μ„œλΉ„μŠ€
        β”œβ”€β”€ scheduler/
        β”‚   └── KakaoWendyScheduler.java     # μŠ€μΌ€μ€„λŸ¬ (λ¦¬λ§ˆμΈλ“œ)
        └── dto/
            β”œβ”€β”€ KakaoRequest.java            # Skill μš”μ²­ DTO
            └── KakaoResponse.java           # Skill 응닡 DTO

핡심 도메인 λͺ¨λΈ

ERD (Entity Relationship)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Vote       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id (PK)         β”‚
β”‚ name            β”‚
β”‚ code (unique)   β”‚
β”‚ startDate       β”‚
β”‚ endDate         β”‚
β”‚ createdAt       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ 1:N
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Participant   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id (PK)         β”‚
β”‚ vote_id (FK)    β”‚
β”‚ displayName     β”‚
β”‚ submitted       β”‚
β”‚ submittedAt     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ 1:N                    1:N
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β–Ό                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ParticipantSelectionβ”‚   β”‚  PriorityPreference β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id (PK)             β”‚   β”‚ id (PK)             β”‚
β”‚ participant_id (FK) β”‚   β”‚ participant_id (FK) β”‚
β”‚ vote_id (FK)        β”‚   β”‚ vote_id (FK)        β”‚
β”‚ date                β”‚   β”‚ date                β”‚
β”‚ period (LUNCH/DINNERβ”‚   β”‚ period              β”‚
β”‚ selected            β”‚   β”‚ priorityIndex (1~3) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ weight              β”‚
                          β”‚ createdAt           β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

μ—”ν‹°ν‹° μ„€λͺ…

μ—”ν‹°ν‹° μ„€λͺ…
Vote νˆ¬ν‘œ μ„Έμ…˜. 고유 code둜 곡유 링크 생성
Participant νˆ¬ν‘œ μ°Έμ—¬μž. Vote에 쒅속
ParticipantSelection μ°Έμ—¬μžμ˜ λ‚ μ§œ/μ‹œκ°„λŒ€ 선택 (true/false)
PriorityPreference μ°Έμ—¬μžμ˜ μš°μ„ μˆœμœ„ (1μˆœμœ„, 2μˆœμœ„, 3μˆœμœ„)

API μ—”λ“œν¬μΈνŠΈ

Vote API (/votes)

Method Endpoint μ„€λͺ…
GET /votes 전체 νˆ¬ν‘œ λͺ©λ‘ 쑰회
GET /votes/{id} νˆ¬ν‘œ 상세 쑰회
GET /votes/share/{code} 곡유 μ½”λ“œλ‘œ νˆ¬ν‘œ 쑰회
POST /votes μƒˆ νˆ¬ν‘œ 생성
PATCH /votes/{id} νˆ¬ν‘œ 정보 μˆ˜μ •
DELETE /votes/{id} νˆ¬ν‘œ μ‚­μ œ
GET /votes/{voteId}/dateRange λ‚ μ§œ λ²”μœ„ 쑰회
GET /votes/{voteId}/result νˆ¬ν‘œ κ²°κ³Ό 쑰회

Participant API

Method Endpoint μ„€λͺ…
GET /votes/{voteId}/participants μ°Έμ—¬μž λͺ©λ‘ 쑰회
POST /votes/{voteId}/participants μ°Έμ—¬μž μΆ”κ°€
PATCH /participants/{id} μ°Έμ—¬μž 정보 μˆ˜μ •
DELETE /participants/{id} μ°Έμ—¬μž μ‚­μ œ
GET /participants/{id}/choices μ°Έμ—¬μž 선택 정보 쑰회
PATCH /participants/{id}/schedule 일정 제좜
POST /participants/{id} μš°μ„ μˆœμœ„ μ„€μ •

Kakao Skill API (/kakao/skill)

Method Endpoint μ„€λͺ…
POST /kakao/skill/start 웬디 μ‹œμž‘ (μ„Έμ…˜ 생성)
POST /kakao/skill/participants μ°Έμ„μž μΆ”κ°€
POST /kakao/skill/weeks μ£Όμ°¨ 선택 및 νˆ¬ν‘œ 생성
POST /kakao/skill/revote μž¬νˆ¬ν‘œ
POST /kakao/skill/end μ„Έμ…˜ μ’…λ£Œ
POST /kakao/skill/status ν˜„μž¬ μƒνƒœ 쑰회
POST /kakao/skill/help 도움말

λ©€ν‹° ν”Œλž«νΌ μ•„ν‚€ν…μ²˜

ν”Œλž«νΌλ³„ νŠΉμ„± 비ꡐ

ꡬ뢄 Discord Kakao
톡신 방식 WebSocket (JDA) REST API (Skill Server)
μ„Έμ…˜ μ‹λ³„μž channelId userKey
μ•Œλ¦Ό 방식 Push (봇이 직접 전솑) Pull (μ‚¬μš©μž μš”μ²­ μ‹œ 응닡)
λ©€ν‹° μœ μ € 채널 λ‚΄ λ‹€μˆ˜ μ°Έμ—¬ 개인 μ±— 기반
이벀트 처리 ListenerAdapter REST Controller

μ•„ν‚€ν…μ²˜ λ‹€μ΄μ–΄κ·Έλž¨

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚           Core Services (meet/)         β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
                    β”‚  β”‚ VoteService β”‚  β”‚ ParticipantSvc  β”‚   β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
                    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
                    β”‚  β”‚VoteResultSvcβ”‚  β”‚  PriorityServiceβ”‚   β”‚
                    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β–²
                                       β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                        β”‚                        β”‚
              β–Ό                        β”‚                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Discord Module       β”‚           β”‚           β”‚      Kakao Module        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β”‚           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ DiscordWendyCommand      β”‚           β”‚           β”‚ KakaoSkillController     β”‚
β”‚ (ListenerAdapter)        β”‚           β”‚           β”‚ (REST Controller)        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β”‚           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ DiscordWendyService      β”‚           β”‚           β”‚ KakaoWendyService        β”‚
β”‚ (channelId 기반 μ„Έμ…˜)    β”‚           β”‚           β”‚ (userKey 기반 μ„Έμ…˜)      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β”‚           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ DiscordWendyScheduler    β”‚           β”‚           β”‚ KakaoWendyScheduler      β”‚
β”‚ DiscordWendyNotifier     β”‚           β”‚           β”‚ KakaoNotifier            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                        β”‚                        β”‚
              β–Ό                        β”‚                        β–Ό
       Discord Server                  β”‚                  Kakao Talk
       (WebSocket)                     β”‚                  (REST API)

Discord Bot (웬디)

μ•„ν‚€ν…μ²˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚DiscordWendyCommand│────▢│DiscordWendyService│────▢│   VoteService   β”‚
β”‚ (ListenerAdapter  β”‚     β”‚ Impl              β”‚     β”‚   Participant   β”‚
β”‚  이벀트 ν•Έλ“€λŸ¬)   β”‚     β”‚ (μ„Έμ…˜ 관리)       β”‚     β”‚   Service       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                        β”‚
         β–Ό                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚DiscordWendyScheduler────▢│DiscordWendyNotifierβ”‚
β”‚ (μ‹œκ°„ 기반       β”‚     β”‚ (λ©”μ‹œμ§€ 전솑)     β”‚
β”‚  νƒœμŠ€ν¬ 관리)    β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

λͺ…λ Ήμ–΄

λͺ…λ Ήμ–΄ μ„€λͺ…
웬디 μ‹œμž‘ 일정 쑰율 μ„Έμ…˜ μ‹œμž‘
웬디 μ’…λ£Œ μ„Έμ…˜ μ’…λ£Œ
웬디 μž¬νˆ¬ν‘œ 동일 μ°Έμ„μžλ‘œ μƒˆ νˆ¬ν‘œ 생성
웬디 도움말 / /help 도움말 ν‘œμ‹œ

μ•Œλ¦Ό μŠ€μΌ€μ€„

μ‹œκ°„ μ•Œλ¦Ό λ‚΄μš©
10λΆ„ ν›„ νˆ¬ν‘œ ν˜„ν™© 곡유
15λΆ„ ν›„ λ―Ένˆ¬ν‘œμž 독촉 (1μ°¨)
1μ‹œκ°„ ν›„ λ―Ένˆ¬ν‘œμž 독촉 (2μ°¨)
6μ‹œκ°„ ν›„ λ―Ένˆ¬ν‘œμž 독촉 (3μ°¨)
12μ‹œκ°„ ν›„ λ―Ένˆ¬ν‘œμž 독촉 (4μ°¨)
24μ‹œκ°„ ν›„ μ΅œν›„ν†΅μ²© (1μˆœμœ„λ‘œ ν™•μ • μ•ˆλ‚΄)

μ„Έμ…˜ 관리 (DiscordWendyServiceImpl)

// 채널별 μƒνƒœ 관리
private final Set<String> activeSessions;           // ν™œμ„± μ„Έμ…˜
private final Map<String, Map<String, String>> participants;  // μ°Έμ„μž
private final Map<String, Long> channelVoteId;      // 채널 -> νˆ¬ν‘œID
private final Map<String, String> channelShareUrl;  // 채널 -> 곡유URL

Kakao Chatbot (웬디)

μ•„ν‚€ν…μ²˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚KakaoSkillController────▢│ KakaoWendyService │────▢│   VoteService   β”‚
β”‚ (REST API         β”‚     β”‚ (μ„Έμ…˜ 관리)       β”‚     β”‚   Participant   β”‚
β”‚  Skill Server)    β”‚     β”‚                   β”‚     β”‚   Service       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                        β”‚
         β–Ό                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚KakaoWendyScheduler│────▢│  KakaoNotifier    β”‚
β”‚ (μ‹œκ°„ 기반       β”‚     β”‚ (μ•Œλ¦Ό 전솑)       β”‚
β”‚  νƒœμŠ€ν¬ 관리)    β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

μ„Έμ…˜ μƒνƒœ (SessionState)

public enum SessionState {
    IDLE,                  // λŒ€κΈ° μƒνƒœ
    WAITING_PARTICIPANTS,  // μ°Έμ„μž μž…λ ₯ λŒ€κΈ°
    WAITING_WEEKS,         // μ£Όμ°¨ 선택 λŒ€κΈ°
    VOTE_CREATED           // νˆ¬ν‘œ 생성 μ™„λ£Œ
}

μ„Έμ…˜ 관리 (KakaoWendyService)

// userKey 기반 μƒνƒœ 관리
private final Map<String, SessionState> sessionStates;     // μ„Έμ…˜ μƒνƒœ
private final Map<String, Map<String, String>> participants; // μ°Έμ„μž
private final Map<String, Long> userVoteId;                // νˆ¬ν‘œ ID
private final Map<String, String> userShareUrl;            // 곡유 URL
private final Map<String, String> userVoteName;            // νˆ¬ν‘œ 이름

Kakao i Open Builder 연동

  • μš”μ²­ DTO (KakaoRequest): userRequest, action, intent, contexts 포함
  • 응닡 DTO (KakaoResponse): simpleText, textWithQuickReplies, basicCard λ“± λ‹€μ–‘ν•œ 응닡 ν˜•μ‹ 지원

νˆ¬ν‘œ κ²°κ³Ό 집계 둜직

μ •λ ¬ κΈ°μ€€ (VoteResultService)

1μˆœμœ„: νˆ¬ν‘œ μΈμ›μˆ˜ (λ§Žμ„μˆ˜λ‘ μƒμœ„)
2μˆœμœ„: μš°μ„ μˆœμœ„ Index 합계 (μž‘μ„μˆ˜λ‘ μƒμœ„)
3μˆœμœ„: λ‚ μ§œ (λΉ λ₯Όμˆ˜λ‘ μƒμœ„)

μ˜ˆμ‹œ

λ‚ μ§œ μ‹œκ°„λŒ€ 인원 μš°μ„ μˆœμœ„ν•© μˆœμœ„
01/20 LUNCH 5λͺ… 3 1μœ„
01/21 DINNER 5λͺ… 7 2μœ„
01/19 LUNCH 4λͺ… 2 3μœ„

μ„€μ • 파일

application.yaml

spring:
  datasource:
    url: jdbc:postgresql://[RDS_HOST]:5432/workingdead
    username: postgres
    password: ${DB_PASSWORD}

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

  session:
    store-type: jdbc

server:
  port: 8080

discord:
  token: ${DISCORD_TOKEN}

kakao:
  rest-api-key: ${KAKAO_REST_API_KEY}
  admin-key: ${KAKAO_ADMIN_KEY}
  channel-id: ${KAKAO_CHANNEL_ID}

ν™˜κ²½ λ³€μˆ˜

λ³€μˆ˜λͺ… μ„€λͺ…
DB_PASSWORD PostgreSQL λΉ„λ°€λ²ˆν˜Έ
DISCORD_TOKEN Discord Bot 토큰
KAKAO_REST_API_KEY Kakao REST API ν‚€
KAKAO_ADMIN_KEY Kakao Admin ν‚€
KAKAO_CHANNEL_ID Kakao 채널 ID
AWS_ACCESS_KEY_ID AWS μ•‘μ„ΈμŠ€ ν‚€
AWS_SECRET_ACCESS_KEY AWS μ‹œν¬λ¦Ώ ν‚€

λ³΄μ•ˆ μ„€μ •

ν˜„μž¬ μƒνƒœ

// SecurityConfig.java
.authorizeHttpRequests(auth -> auth
    .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
    .requestMatchers("/kakao/skill/**").permitAll()  // Kakao Skill Server
    .anyRequest().permitAll()  // λͺ¨λ“  μš”μ²­ ν—ˆμš©
);
  • 인증: λ―Έκ΅¬ν˜„ (λͺ¨λ“  API 곡개)
  • CSRF: λΉ„ν™œμ„±ν™”
  • CORS: μ§€μ •λœ λ„λ©”μΈλ§Œ ν—ˆμš©

ν—ˆμš© 도메인 (CorsConfig)

  • localhost:3000, 5173, 8080, 8081
  • whend.app (HTTP/HTTPS)
  • whendy.netlify.app

핡심 λΉ„μ¦ˆλ‹ˆμŠ€ ν”Œλ‘œμš°

1. Discord νˆ¬ν‘œ 생성 ν”Œλ‘œμš°

1. λ””μŠ€μ½”λ“œμ—μ„œ "웬디 μ‹œμž‘" μž…λ ₯
2. μ°Έμ„μž 선택 (λ“œλ‘­λ‹€μš΄ 메뉴)
3. μ£Όμ°¨ 선택 (이번 μ£Ό ~ 6μ£Ό λ’€)
4. Vote μ—”ν‹°ν‹° 생성 + Participant 일괄 생성
5. 곡유 URL λ°˜ν™˜ (whendy.netlify.app/v/{code})
6. μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘ (λ¦¬λ§ˆμΈλ“œ μ•Œλ¦Ό)

2. Kakao νˆ¬ν‘œ 생성 ν”Œλ‘œμš°

1. μΉ΄μΉ΄μ˜€ν†‘μ—μ„œ "웬디 μ‹œμž‘" λ°œν™”
2. μ°Έμ„μž 이름 μž…λ ₯ (μ‰Όν‘œ ꡬ뢄)
3. μ£Όμ°¨ 선택 (Quick Reply λ²„νŠΌ)
4. Vote μ—”ν‹°ν‹° 생성 + Participant 일괄 생성
5. 곡유 URL λ°˜ν™˜
6. μŠ€μΌ€μ€„λŸ¬ μ‹œμž‘ (λ¦¬λ§ˆμΈλ“œ μ•Œλ¦Ό)

3. νˆ¬ν‘œ μ°Έμ—¬ ν”Œλ‘œμš° (곡톡)

1. 곡유 URL 접속
2. μ°Έμ—¬μž μΉ© 선택 (본인 선택)
3. λ‚ μ§œ/μ‹œκ°„λŒ€ 선택 (LUNCH/DINNER)
4. μš°μ„ μˆœμœ„ μ„€μ • (1~3μˆœμœ„, 선택사항)
5. 제좜 β†’ ParticipantSelection, PriorityPreference μ €μž₯

4. κ²°κ³Ό 쑰회 ν”Œλ‘œμš°

1. GET /votes/{voteId}/result
2. μ„ νƒλœ 일정 집계 (selected=true)
3. μš°μ„ μˆœμœ„ κ°€μ€‘μΉ˜ 계산
4. μ •λ ¬: 인원 > μš°μ„ μˆœμœ„ν•© > λ‚ μ§œ
5. μƒμœ„ 3개 λž­ν‚Ή λ°˜ν™˜

μ˜μ‘΄μ„± λͺ©λ‘ (build.gradle)

// Core
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Database
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'com.h2database:h2'
implementation 'org.flywaydb:flyway-core'

// Session & Cache
implementation 'org.springframework.session:spring-session-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Discord Bot
implementation 'net.dv8tion:JDA:5.0.0-beta.24'

// API Docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8'

// Utilities
compileOnly 'org.projectlombok:lombok'

ν”„λ‘œμ νŠΈ νŠΉμ§•

  1. λ©€ν‹° ν”Œλž«νΌ 지원: Discord + Kakao λ™μ‹œ 운영
  2. ν”Œλž«νΌ 독립적 μ½”μ–΄: 핡심 νˆ¬ν‘œ λ‘œμ§μ€ 곡유, ν”Œλž«νΌλ³„ μ–΄λŒ‘ν„° 뢄리
  3. μ‹€μ‹œκ°„ μ•Œλ¦Ό: μŠ€μΌ€μ€„λŸ¬ 기반 μžλ™ λ¦¬λ§ˆμΈλ“œ (ν”Œλž«νΌλ³„ κ΅¬ν˜„)
  4. μš°μ„ μˆœμœ„ μ‹œμŠ€ν…œ: λ‹¨μˆœ νˆ¬ν‘œκ°€ μ•„λ‹Œ κ°€μ€‘μΉ˜ 기반 κ²°κ³Ό λ„μΆœ
  5. 곡유 URL: 8자리 고유 μ½”λ“œλ‘œ κ°„νŽΈν•œ 곡유
  6. μ„Έμ…˜ 관리: ν”Œλž«νΌλ³„ 독립적인 μ„Έμ…˜ μƒνƒœ 관리
    • Discord: channelId 기반
    • Kakao: userKey 기반

λ¬Έμ„œ 생성일: 2026-01-19 μ΅œμ’… μ—…λ°μ΄νŠΈ: 2026-01-19 (λ©€ν‹° ν”Œλž«νΌ μ•„ν‚€ν…μ²˜ 반영)