Skip to content

Commit 4f389f9

Browse files
Merge pull request #4 from Palbahngmiyine/main
SOLAPI Kotlin SDK 1.1.0
2 parents 168c16d + be5b4b8 commit 4f389f9

87 files changed

Lines changed: 8191 additions & 257 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/scripts/sync-version.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# 버전 동기화 스크립트
3+
# build.gradle.kts의 버전이 변경되면 README.md와 LLM_GUIDE.md에 반영
4+
5+
set -e
6+
7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
9+
10+
GRADLE_FILE="$PROJECT_ROOT/build.gradle.kts"
11+
README_FILE="$PROJECT_ROOT/README.md"
12+
LLM_GUIDE_FILE="$PROJECT_ROOT/LLM_GUIDE.md"
13+
14+
# build.gradle.kts가 수정된 경우에만 실행
15+
if [[ -n "$CLAUDE_FILE_PATHS" ]]; then
16+
if ! echo "$CLAUDE_FILE_PATHS" | grep -q "build.gradle.kts"; then
17+
exit 0
18+
fi
19+
fi
20+
21+
# build.gradle.kts에서 버전 추출 (예: version = "1.1.0" -> 1.1.0)
22+
VERSION=$(grep -E '^version\s*=' "$GRADLE_FILE" | head -1 | sed -E 's/.*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/')
23+
24+
if [[ -z "$VERSION" ]] || [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
25+
echo "유효한 버전을 찾을 수 없습니다: $VERSION"
26+
exit 1
27+
fi
28+
29+
# README.md 버전 업데이트
30+
if [[ -f "$README_FILE" ]]; then
31+
# com.solapi:sdk:X.X.X 패턴 업데이트
32+
sed -i '' -E "s/(com\.solapi:sdk:)[0-9]+\.[0-9]+\.[0-9]+/\1$VERSION/g" "$README_FILE"
33+
# <version>X.X.X</version> 패턴
34+
sed -i '' -E "s|(<version>)[0-9]+\.[0-9]+\.[0-9]+(</version>)|\1$VERSION\2|g" "$README_FILE"
35+
fi
36+
37+
# LLM_GUIDE.md 버전 업데이트
38+
if [[ -f "$LLM_GUIDE_FILE" ]]; then
39+
sed -i '' -E "s/(com\.solapi:sdk:)[0-9]+\.[0-9]+\.[0-9]+/\1$VERSION/g" "$LLM_GUIDE_FILE"
40+
sed -i '' -E "s|(<version>)[0-9]+\.[0-9]+\.[0-9]+(</version>)|\1$VERSION\2|g" "$LLM_GUIDE_FILE"
41+
fi
42+
43+
echo "버전 $VERSION 동기화 완료"

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SOLAPI API Credentials
2+
SOLAPI_API_KEY=
3+
SOLAPI_API_SECRET=
4+
5+
# Kakao Business Channel
6+
KAKAO_PF_ID=
7+
8+
# Phone Numbers (Optional)
9+
SENDER_NUMBER=
10+
TEST_PHONE_NUMBER=

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
# E2E 테스트에 필요한 환경변수 (선택사항 - 설정되지 않으면 E2E 테스트 건너뜀)
13+
#
14+
# 기본 환경변수:
15+
# SOLAPI_API_KEY - Solapi API 키
16+
# SOLAPI_API_SECRET - Solapi API 시크릿
17+
# SOLAPI_SENDER - 등록된 발신번호
18+
# SOLAPI_RECIPIENT - 테스트 수신번호
19+
#
20+
# 카카오 테스트 환경변수 (선택):
21+
# SOLAPI_KAKAO_PF_ID - 카카오 비즈니스 채널 ID
22+
# SOLAPI_KAKAO_TEMPLATE_ID - 카카오 알림톡 템플릿 ID
23+
24+
jobs:
25+
build:
26+
runs-on: ubuntu-latest
27+
strategy:
28+
matrix:
29+
java-version: ['21']
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Set up JDK ${{ matrix.java-version }}
35+
uses: actions/setup-java@v4
36+
with:
37+
java-version: ${{ matrix.java-version }}
38+
distribution: temurin
39+
40+
- name: Setup Gradle
41+
uses: gradle/actions/setup-gradle@v4
42+
43+
- name: Build
44+
run: ./gradlew build -x test
45+
46+
- name: Test
47+
run: ./gradlew test
48+
env:
49+
SOLAPI_API_KEY: ${{ secrets.SOLAPI_API_KEY }}
50+
SOLAPI_API_SECRET: ${{ secrets.SOLAPI_API_SECRET }}
51+
SOLAPI_SENDER: ${{ secrets.SOLAPI_SENDER }}
52+
SOLAPI_RECIPIENT: ${{ secrets.SOLAPI_RECIPIENT }}
53+
SOLAPI_KAKAO_PF_ID: ${{ secrets.SOLAPI_KAKAO_PF_ID }}
54+
SOLAPI_KAKAO_TEMPLATE_ID: ${{ secrets.SOLAPI_KAKAO_TEMPLATE_ID }}

.github/workflows/publish.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Publish to Maven Central
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
# 배포에 필요한 Secrets:
11+
# MAVEN_CENTRAL_USERNAME - Central Portal User Token username
12+
# MAVEN_CENTRAL_TOKEN - Central Portal User Token password
13+
# GPG_SIGNING_KEY - GPG 비밀 키 (armor 형식)
14+
# GPG_SIGNING_PASSWORD - GPG 키 비밀번호
15+
#
16+
# E2E 테스트에 필요한 환경변수 (선택사항 - 설정되지 않으면 E2E 테스트 건너뜀)
17+
#
18+
# 기본 환경변수:
19+
# SOLAPI_API_KEY - Solapi API 키
20+
# SOLAPI_API_SECRET - Solapi API 시크릿
21+
# SOLAPI_SENDER - 등록된 발신번호
22+
# SOLAPI_RECIPIENT - 테스트 수신번호
23+
#
24+
# 카카오 테스트 환경변수 (선택):
25+
# SOLAPI_KAKAO_PF_ID - 카카오 비즈니스 채널 ID
26+
# SOLAPI_KAKAO_TEMPLATE_ID - 카카오 알림톡 템플릿 ID
27+
28+
jobs:
29+
publish:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Checkout
33+
uses: actions/checkout@v4
34+
35+
- name: Set up JDK 21
36+
uses: actions/setup-java@v4
37+
with:
38+
java-version: 21
39+
distribution: temurin
40+
41+
- name: Setup Gradle
42+
uses: gradle/actions/setup-gradle@v4
43+
44+
- name: Run tests
45+
run: ./gradlew test
46+
env:
47+
SOLAPI_API_KEY: ${{ secrets.SOLAPI_API_KEY }}
48+
SOLAPI_API_SECRET: ${{ secrets.SOLAPI_API_SECRET }}
49+
SOLAPI_SENDER: ${{ secrets.SOLAPI_SENDER }}
50+
SOLAPI_RECIPIENT: ${{ secrets.SOLAPI_RECIPIENT }}
51+
SOLAPI_KAKAO_PF_ID: ${{ secrets.SOLAPI_KAKAO_PF_ID }}
52+
SOLAPI_KAKAO_TEMPLATE_ID: ${{ secrets.SOLAPI_KAKAO_TEMPLATE_ID }}
53+
54+
- name: Publish to Maven Central
55+
run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
56+
env:
57+
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
58+
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
59+
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }}
60+
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSWORD }}

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ signing.gpg
3434
*.gpg
3535
*.asc
3636
secret.key
37+
.env
38+
.env.local
3739

3840
# Generated files
3941
/docs/
40-
manual/
42+
manual/
43+
44+
# OMO, OMC
45+
.sisyphus/
46+
.omc/

AGENTS.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# SOLAPI Kotlin SDK - Knowledge Base
2+
3+
**Generated:** 2026-01-27 | **Commit:** 618f129 | **Branch:** main
4+
5+
## CRITICAL: Development Principles
6+
7+
**MUST follow `CLAUDE.md` development principles:**
8+
9+
| Principle | Rule |
10+
|-----------|------|
11+
| **Tidy First** | NEVER mix structural and behavioral changes in a single commit |
12+
| **Commit Separation** | `refactor:` (structural) vs `feat:`/`fix:` (behavioral) in separate commits |
13+
| **TDD** | Write tests first (Red → Green → Refactor) |
14+
| **Single Responsibility** | Classes/methods have single responsibility only |
15+
| **Tidy Code First** | Clean up target area code before adding features |
16+
17+
```bash
18+
# Correct commit order
19+
git commit -m "refactor: extract validation logic to separate method"
20+
git commit -m "feat: add phone number format validation"
21+
22+
# Forbidden (mixed commit)
23+
git commit -m "feat: add validation and refactor code" # ❌ FORBIDDEN
24+
```
25+
26+
---
27+
28+
## OVERVIEW
29+
30+
Kotlin/Java SDK for SOLAPI messaging platform. Supports SMS, LMS, MMS, Kakao Alimtalk/Brand Message, Naver Smart Notification, RCS, Fax, and Voice messaging.
31+
32+
## STRUCTURE
33+
34+
```
35+
src/main/java/com/solapi/sdk/
36+
├── SolapiClient.kt # Entry point (use this)
37+
├── NurigoApp.kt # DEPRECATED - do not use
38+
└── message/
39+
├── service/ # API operations (send, query, templates)
40+
├── model/ # Domain models (Message, options)
41+
│ └── kakao/ # 19 files - Kakao templates, buttons, options
42+
├── dto/ # Request/Response DTOs
43+
├── exception/ # Exception hierarchy (8 types)
44+
└── lib/ # Internal utilities (auth, helpers)
45+
```
46+
47+
## WHERE TO LOOK
48+
49+
| Task | Location | Notes |
50+
|------|----------|-------|
51+
| **Initialize SDK** | `SolapiClient.kt` | `createInstance(apiKey, secretKey)` |
52+
| **Send messages** | `service/DefaultMessageService.kt` | `send(message)` or `send(messages)` |
53+
| **Query messages** | `service/DefaultMessageService.kt` | `getMessageList(params)` |
54+
| **Upload files** | `service/DefaultMessageService.kt` | `uploadFile(file, type)` for MMS/Fax |
55+
| **Kakao Alimtalk** | `service/DefaultMessageService.kt` | 11 template methods |
56+
| **Create Message** | `model/Message.kt` | Data class with all message options |
57+
| **Kakao options** | `model/kakao/KakaoOption.kt` | Alimtalk/FriendTalk config |
58+
| **Handle errors** | `exception/` | Catch specific `Solapi*Exception` types |
59+
| **HTTP layer** | `service/MessageHttpService.kt` | Retrofit interface (internal) |
60+
| **Auth** | `lib/Authenticator.kt` | HMAC-SHA256 (internal, auto-injected) |
61+
62+
## CODE PATTERNS
63+
64+
### Serialization
65+
```kotlin
66+
@Serializable
67+
data class Message(
68+
var to: String? = null,
69+
var from: String? = null,
70+
// All fields nullable with defaults for flexibility
71+
)
72+
```
73+
- **ALWAYS** use `@Serializable` annotation
74+
- **ALWAYS** use `kotlinx.serialization` (not Jackson/Gson)
75+
- **ALWAYS** provide nullable fields with defaults
76+
77+
### Service Methods
78+
```kotlin
79+
@JvmOverloads // Java interop
80+
@Throws(SolapiMessageNotReceivedException::class, ...)
81+
fun send(messages: List<Message>, config: SendRequestConfig? = null): MultipleDetailMessageSentResponse
82+
```
83+
- **ALWAYS** annotate with `@JvmOverloads` for optional params
84+
- **ALWAYS** declare `@Throws` for checked exceptions
85+
86+
### Exception Handling
87+
```kotlin
88+
// Internal: Map error codes to exceptions
89+
when (errorResponse.errorCode) {
90+
"ValidationError" -> throw SolapiBadRequestException(msg)
91+
"InvalidApiKey" -> throw SolapiInvalidApiKeyException(msg)
92+
else -> throw SolapiUnknownException(msg)
93+
}
94+
```
95+
- Exceptions are `sealed interface` based (SolapiException)
96+
- 8 specific exception types
97+
98+
### Phone Number Normalization
99+
```kotlin
100+
init {
101+
from = from?.replace("-", "")
102+
to = to?.replace("-", "")
103+
}
104+
```
105+
- Dashes auto-stripped from phone numbers in `Message.init`
106+
107+
### Test Conventions
108+
```kotlin
109+
import kotlin.test.Test
110+
import kotlin.test.assertEquals
111+
import kotlin.test.assertTrue
112+
import kotlin.test.assertFailsWith
113+
114+
class AuthenticatorTest {
115+
@Test
116+
fun `generateAuthInfo returns HMAC-SHA256 format`() {
117+
// Given
118+
val authenticator = Authenticator("api-key", "secret")
119+
// When
120+
val result = authenticator.generateAuthInfo()
121+
// Then
122+
assertTrue(result.startsWith("HMAC-SHA256 "))
123+
}
124+
}
125+
```
126+
- **ALWAYS** use `kotlin.test` (NOT JUnit directly)
127+
- **ALWAYS** use Given-When-Then comment structure
128+
- **ALWAYS** use backtick method names for readability
129+
130+
## ANTI-PATTERNS
131+
132+
| Forbidden | Required |
133+
|-----------|----------|
134+
| `NurigoApp.initialize()` | `SolapiClient.createInstance()` |
135+
| Direct `DefaultMessageService()` | Use factory via `SolapiClient` |
136+
| Catch generic `Exception` | Catch specific `Solapi*Exception` |
137+
| `net.nurigo.sdk` imports | `com.solapi.sdk` package only |
138+
| Jackson/Gson serialization | `kotlinx.serialization` only |
139+
| Mixed structural+behavioral commits | Separate commits per Tidy First |
140+
141+
## INTERNAL CLASSES (Do Not Use Directly)
142+
143+
- `Authenticator` - HMAC auth (auto-injected via interceptor)
144+
- `ErrorResponse` - Internal error DTO
145+
- `MessageHttpService` - Retrofit interface
146+
- `JsonSupport` - Serialization config
147+
- `MapHelper`, `Criterion` - Internal utilities
148+
149+
## EXCEPTION TYPES
150+
151+
| Exception | When Thrown |
152+
|-----------|-------------|
153+
| `SolapiApiKeyException` | Empty/missing API key |
154+
| `SolapiInvalidApiKeyException` | Invalid credentials |
155+
| `SolapiBadRequestException` | Validation error, bad input |
156+
| `SolapiEmptyResponseException` | Server returned empty body |
157+
| `SolapiFileUploadException` | File upload failed |
158+
| `SolapiMessageNotReceivedException` | All messages failed (has `failedMessageList`) |
159+
| `SolapiUnknownException` | Unclassified server error |
160+
161+
## KAKAO INTEGRATION
162+
163+
19 model files in `model/kakao/`:
164+
- `KakaoOption` - Main config (pfId, templateId, variables)
165+
- `KakaoAlimtalkTemplate*` - Template CRUD models
166+
- `KakaoBrandMessageTemplate` - Brand message with carousels
167+
- `KakaoButton`, `KakaoButtonType` - Button configurations
168+
169+
Template workflow: `getKakaoAlimtalkTemplateCategories()``createKakaoAlimtalkTemplate()``requestKakaoAlimtalkTemplateInspection()`
170+
171+
## BUILD & TEST
172+
173+
```bash
174+
./gradlew clean build test # Full build
175+
./gradlew test # Tests only
176+
./gradlew shadowJar # Fat JAR with relocated deps
177+
```
178+
179+
**Shadow JAR**: Dependencies relocated to `com.solapi.shadow.*` to prevent conflicts.
180+
181+
## NOTES
182+
183+
- **Java 8 target**: Code must work on JVM 1.8
184+
- **Source location**: Kotlin in `src/main/java/` (unconventional but intentional)
185+
- **Tests**: `src/test/kotlin/`, `kotlin.test` (Kotlin native), Given-When-Then style
186+
- **Version**: Auto-generated at `build/generated/source/kotlin/com/solapi/sdk/Version.kt`
187+
- **Docs**: Dokka output to `./docs/`, run `./gradlew dokkaGeneratePublicationHtml`

0 commit comments

Comments
 (0)