1- # SOLAPI SDK for Node.js
1+ # AGENTS.md
22
3- ** Generated:** 2026-01-21
4- ** Commit:** 9df35df
5- ** Branch:** master
3+ SOLAPI SDK for Node.js. Effect 라이브러리 기반 함수형 프로그래밍 + 타입 안전 에러 처리.
64
7- ## OVERVIEW
8-
9- Server-side SDK for SMS/LMS/MMS and Kakao messaging in Korea. Uses Effect library for type-safe functional programming with Data.TaggedError-based error handling.
10-
11- ## STRUCTURE
5+ ## Structure
126
137```
148solapi-nodejs/
159├── src/
1610│ ├── index.ts # SolapiMessageService facade (entry point)
1711│ ├── errors/ # Data.TaggedError types
1812│ ├── lib/ # Core utilities (fetcher, auth, error handler)
19- │ ├── models/ # Schemas, requests, responses (see models/AGENTS.md)
20- │ ├── services/ # Domain services (see services/AGENTS.md)
13+ │ ├── models/ # Schemas, requests, responses
14+ │ ├── services/ # Domain services
2115│ └── types/ # Shared type definitions
2216├── test/ # Mirrors src/ structure
2317├── examples/ # Usage examples (excluded from build)
2418└── debug/ # Debug scripts
2519```
2620
27- ## WHERE TO LOOK
21+ ## Where to Look
2822
2923| Task | Location | Notes |
3024| ------| ----------| -------|
@@ -36,58 +30,160 @@ solapi-nodejs/
3630| Fix API request issue | ` src/lib/defaultFetcher.ts ` | HTTP client with retry |
3731| Understand error flow | ` src/lib/effectErrorHandler.ts ` | Effect → Promise conversion |
3832
39- ## CONVENTIONS
33+ ## Conventions
34+
35+ ### Effect Library (Mandatory)
36+
37+ ** Async operations** : ` Effect.tryPromise ` 또는 ` Effect.gen `
38+ ``` typescript
39+ Effect .tryPromise ({
40+ try : () => fetch (url , options ),
41+ catch : e => new NetworkError ({ url , cause: e }),
42+ });
43+ ```
44+
45+ ** Complex flow** : ` Effect.gen `
46+ ``` typescript
47+ Effect .gen (function * (_ ) {
48+ const auth = yield * _ (buildAuth (params ));
49+ const response = yield * _ (fetchWithRetry (url , auth ));
50+ return yield * _ (parseResponse (response ));
51+ });
52+ ```
53+
54+ ** Error to Promise** : 반드시 ` runSafePromise ` 경유
55+ ``` typescript
56+ return runSafePromise (effect );
57+ // BAD: try { await Effect.runPromise(...) } catch { }
58+ ```
59+
60+ ### Service Pattern
61+
62+ ` DefaultService ` 상속 → ` this.request() ` 사용:
63+ ``` typescript
64+ export default class MyService extends DefaultService {
65+ async myMethod(data : Request ): Promise <Response > {
66+ return this .request <Request , Response >({
67+ httpMethod: ' POST' ,
68+ url: ' my/endpoint' ,
69+ body: data ,
70+ });
71+ }
72+ }
73+ ```
74+
75+ Effect.gen 활용 (복잡한 로직):
76+ ``` typescript
77+ async send (messages : Request ): Promise < Response > {
78+ const effect = Effect .gen (function * (_ ) {
79+ const validated = yield * _ (validateSchema (messages ));
80+ return yield * _ (Effect .promise (() => this .request (... )));
81+ });
82+ return runSafePromise(effect );
83+ }
84+ ```
85+
86+ ### Model Pattern
87+
88+ Three-layer architecture: ` base/ ` (도메인) → ` requests/ ` (입력 변환) → ` responses/ ` (API 응답)
89+
90+ ** Type + Schema** :
91+ ``` typescript
92+ export type MyType = Schema .Schema .Type <typeof mySchema >;
93+ export const mySchema = Schema .Struct ({
94+ field: Schema .String ,
95+ optional: Schema .optional (Schema .Number ),
96+ });
97+ ```
98+
99+ ** Discriminated Union** :
100+ ``` typescript
101+ export const buttonSchema = Schema .Union (
102+ webButtonSchema , // { linkType: 'WL', ... }
103+ appButtonSchema , // { linkType: 'AL', ... }
104+ );
105+ ```
40106
41- ** Effect Library (MANDATORY)** :
42- - All errors: ` Data.TaggedError ` with environment-aware ` toString() `
43- - Async operations: ` Effect.gen ` + ` Effect.tryPromise ` , never wrap with try-catch
44- - Validation: ` Effect Schema ` with ` Schema.filter ` , ` Schema.transform `
45- - Error execution: ` runSafePromise() ` / ` runSafeSync() ` from effectErrorHandler
107+ ** Custom Validation** :
108+ ``` typescript
109+ Schema .String .pipe (
110+ Schema .filter (isValid , { message : () => ' Error message' }),
111+ );
112+ ```
46113
47- ** TypeScript** :
48- - ** NEVER use ` any ` ** — use ` unknown ` + type guards or Effect Schema
49- - Strict mode enforced (` noUnusedLocals ` , ` noUnusedParameters ` )
50- - Path aliases: ` @models ` , ` @lib ` , ` @services ` , ` @errors ` , ` @internal-types `
114+ ### Lib Utilities
51115
52- ** Testing** :
53- - Unit: ` vitest ` with ` Schema.decodeUnknownEither() ` for validation tests
54- - E2E: ` @effect/vitest ` with ` it.effect() ` and ` Effect.gen `
55- - Run: ` pnpm test ` / ` pnpm test:watch `
116+ | File | Purpose |
117+ | ------| ---------|
118+ | ` defaultFetcher.ts ` | HTTP client — Effect.gen, retry 3x exponential backoff, Match |
119+ | ` effectErrorHandler.ts ` | ` runSafePromise ` , ` runSafeSync ` , ` unwrapCause ` |
120+ | ` authenticator.ts ` | HMAC-SHA256 auth header |
121+ | ` stringifyQuery.ts ` | URL query string builder (array handling) |
122+ | ` fileToBase64.ts ` | File/URL → Base64 |
123+ | ` stringDateTrasnfer.ts ` | Date parsing with ` InvalidDateError ` |
56124
57- ## ANTI-PATTERNS
125+ ## Anti-Patterns
58126
59127| Pattern | Why Bad | Do Instead |
60128| ---------| ---------| ------------|
61129| ` any ` type | Loses type safety | ` unknown ` + type guards |
62130| ` as any ` , ` @ts-ignore ` | Suppresses errors | Fix the type issue |
63- | try-catch around Effect | Loses Effect benefits | Use ` Effect.catchTag ` |
64- | Direct ` throw new Error() ` | Inconsistent error handling | Use ` Data.TaggedError ` |
131+ | try-catch around Effect | Loses Effect benefits | ` Effect.catchTag ` |
132+ | Direct ` throw new Error() ` | Inconsistent error handling | ` Data.TaggedError ` |
65133| Empty catch blocks | Swallows errors | Handle or propagate |
134+ | Bypass ` runSafePromise ` | Loses error formatting | Always use ` runSafePromise ` |
135+ | Call ` defaultFetcher ` directly | Bypasses service layer | Use ` this.request() ` |
136+ | Skip schema validation | Runtime errors | Always validate input |
137+ | Interface when schema needed | No runtime validation | Use ` Schema.Struct ` |
138+ | Duplicate validation logic | Inconsistency | Compose schemas |
139+ | Hardcode API URL | Inflexible | Use ` DefaultService.baseUrl ` |
140+ | Mix Effect and Promise styles | Confusing | Pick one per method |
66141
67- ## COMMANDS
142+ ## Architecture Notes
68143
69- ``` bash
70- pnpm dev # Watch mode (tsup)
71- pnpm build # Lint + build
72- pnpm lint # Biome check with auto-fix
73- pnpm test # Run tests once
74- pnpm test:watch # Watch mode
75- pnpm docs # Generate TypeDoc
76- ```
77-
78- ## ARCHITECTURE NOTES
79-
80- ** Service Facade Pattern** : ` SolapiMessageService ` aggregates 7 domain services via ` bindServices() ` dynamic method binding. All services extend ` DefaultService ` .
144+ ** Service Facade** : ` SolapiMessageService ` 가 7개 도메인 서비스를 ` bindServices() ` 로 동적 바인딩.
81145
82146** Error Flow** :
83147```
84- API Response
85- → defaultFetcher (creates Effect errors)
86- → runSafePromise (converts to Promise)
87- → toCompatibleError (preserves properties on Error)
88- → Consumer
148+ API Response → defaultFetcher (Effect errors) → runSafePromise (Promise)
149+ → 원본 Data.TaggedError 그대로 reject → Consumer
89150```
90151
91- ** Production vs Development** : Error messages stripped of stack traces and detailed context in production (` process.env.NODE_ENV === 'production' ` ).
152+ ** Production vs Development** : Production에서는 stack trace와 상세 컨텍스트가 제거됨.
153+
154+ ** Retry Logic** : ` defaultFetcher.ts ` — 3회 재시도, exponential backoff (connection refused, reset, 503).
155+
156+ ## Testing Guidelines (Detail)
157+
158+ ### Failure Injection
159+ - 의존성 실패 시뮬레이션 (첫 호출, N번째 호출, 지속적 실패)
160+ - 타임아웃, 취소 케이스 포함
161+ - 부분 성공 후 실패 시나리오
162+
163+ ### Concurrency
164+ - Race condition 없음 확인
165+ - Deadlock 없음 확인
166+ - 중복 실행 없음 확인
167+
168+ ### Persistence
169+ - Atomic behavior (전부 또는 전무)
170+ - 중간 상태 오염 없음
171+ - 안전한 재시도 및 복구
172+
173+ ### Fuzz (권장)
174+ - 입력 파싱/디코딩에 fuzz 테스트 적용
175+ - panic이나 무한 리소스 사용 없음 확인
176+
177+ ### Style
178+ - 테이블 기반 테스트: ` it.each() ` 활용
179+ - 외부 의존성: fake/stub 사용
180+ - cleanup hooks (` afterEach ` /` afterAll ` )
181+
182+ ## Sub-Agents
183+
184+ ### tidy-first
185+ Kent Beck의 "Tidy First?" 원칙 적용 리팩토링 전문가.
186+ ` .claude/agents/tidy-first.md ` 참조.
92187
93- ** Retry Logic** : ` defaultFetcher.ts ` implements 3x retry with exponential backoff for retryable errors (connection refused, reset, 503).
188+ ** 자동 호출** : 기능 추가, 동작 구현, 코드 리뷰, 리팩토링 작업 시.
189+ ** 핵심 규칙** : 구조적 변경과 동작 변경을 항상 분리.
0 commit comments