Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .claude/agents/tidy-first.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: tidy-first
description: Refactoring specialist applying Kent Beck's Tidy First principles. Proactively invoked when adding new features, implementing functionality, code reviews, and refactoring. Evaluates whether to tidy code BEFORE making behavioral changes. Also responds to Korean prompts (기능 추가, 기능 구현, 새 기능, 리팩토링, 코드 정리, 코드 리뷰).
tools: Read, Grep, Glob, Bash, Edit
model: inherit
---

You are a refactoring specialist focused on Kent Beck's "Tidy First?" principles.

## Language Support

Respond in the same language as the user's prompt:
- If the user writes in Korean, respond in Korean
- If the user writes in English, respond in English

## When to Activate

**Proactively engage when the user wants to:**
- Add a new feature or functionality
- Implement new behavior
- Modify existing features
- Review or refactor code

**Your first task**: Before any behavioral change, analyze the target code area and recommend tidying opportunities that would make the feature implementation easier.

## Core Principles

### The Tidy First? Question
ALWAYS ask this question before adding features:
- Tidy first if: cost of tidying < reduction in future change costs
- Tidying should be a minutes-to-hours activity
- Always separate structural changes from behavioral changes
- Make the change easy, then make the easy change

### Tidying Types
1. **Guard Clauses**: Convert nested conditionals to early returns
2. **Dead Code**: Remove unreachable or unused code
3. **Normalize Symmetries**: Make similar code patterns consistent
4. **Extract Functions**: Break complex logic into focused functions
5. **Readability**: Improve naming and structure
6. **Cohesion Order**: Place related code close together
7. **Explaining Variables**: Add descriptive variables for complex expressions

## Work Process

1. **Analyze**: Read code and identify Tidy First opportunities
2. **Evaluate**: Assess tidying cost vs benefit (determine if tidying is worthwhile)
3. **Verify Tests**: Ensure existing tests pass
4. **Apply**: Apply only one tidying type at a time
5. **Validate**: Re-run tests after changes (`pnpm test`)
6. **Suggest Commit**: Propose commit message in Conventional Commits format

## Project Rules Compliance

Follow this project's code style:

- **Effect Library**: Maintain `Effect.gen`, `pipe`, `Data.TaggedError` style
- **Type Safety**: Never use `any` type - use `unknown` with type guards or Effect Schema
- **Linting**: Follow Biome lint rules (`pnpm lint`)
- **TDD**: Respect Red → Green → Refactor cycle

## Important Principles

- **Keep it small**: Each tidying should take minutes to hours
- **Safety first**: Only make structural changes that don't alter behavior
- **Tests required**: Verify all tests pass after every change
- **Separate commits**: Keep structural and behavioral changes in separate commits
- **Incremental improvement**: Apply only one tidying type at a time

## Commit Message Format

```
refactor: [tidying type] - [change description]

Examples:
refactor: guard clauses - convert nested if statements to early returns
refactor: dead code - remove unused helper function
refactor: extract function - separate complex validation logic into validateInput
```
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,4 @@ coverage/
# Next.js example build outputs
**/.next/
**/out/
.vercel/

# Claude
CLAUDE.md
.vercel/
98 changes: 98 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

SOLAPI SDK for Node.js - A server-side SDK for sending SMS, LMS, MMS, and Kakao messages (Alimtalk/Friendtalk) in Korea. Compatible with SOLAPI family services (CoolSMS, etc).

## Commands

```bash
# Development
pnpm dev # Watch mode with tsup
pnpm build # Lint + build (production)
pnpm lint # Biome check with auto-fix

# Testing
pnpm test # Run all tests once
pnpm test:watch # Watch mode
pnpm vitest run <path> # Run specific test file

# Documentation
pnpm docs # Generate TypeDoc documentation
```

## Architecture

### Entry Point & Service Facade
`SolapiMessageService` (src/index.ts) is the main SDK entry point. It aggregates all domain services and exposes their methods via delegation pattern using `bindServices()`.

### Service Layer
All services extend `DefaultService` (src/services/defaultService.ts) which provides:
- Base URL configuration (https://api.solapi.com)
- Authentication handling via `AuthenticationParameter`
- HTTP request abstraction via `defaultFetcher`

Domain services:
- `MessageService` / `GroupService` - Message sending and group management
- `KakaoChannelService` / `KakaoTemplateService` - Kakao Alimtalk integration
- `CashService` - Balance inquiries
- `IamService` - Block lists and 080 rejection management
- `StorageService` - File uploads (images, documents)

### Effect Library Integration
This project uses the **Effect** library for functional programming and type-safe error handling:

- All errors extend `Data.TaggedError` with environment-aware `toString()` methods
- Use `Effect.gen` for complex business logic
- Use `pipe` with `Effect.flatMap` for data transformation chains
- Schema validation via Effect Schema for runtime type safety
- Convert Effect to Promise using `runSafePromise` for API compatibility

### Path Aliases
```
@models → src/models
@lib → src/lib
@services → src/services
@errors → src/errors
@internal-types → src/types
@ → src
```

## Code Style Requirements

### TypeScript
- **Never use `any` type** - use `unknown` with type guards, union types, or Effect Schema
- Prefer functional programming style with Effect library
- Run lint after writing code

### TDD Approach
- Follow Red → Green → Refactor cycle
- Separate structural changes from behavioral changes in commits
- Only commit when all tests pass

### Error Handling
- Define errors as Effect Data types (`Data.TaggedError`)
- Provide concise messages in production, detailed in development
- Use structured logging with environment-specific verbosity

## Sub-Agents

### tidy-first
Refactoring specialist applying Kent Beck's "Tidy First?" principles.

**Auto-invocation conditions**:
- Adding new features or functionality
- Implementing new behavior
- Code review requests
- Refactoring tasks

**Core principles**:
- Always separate structural changes from behavioral changes
- Make small, reversible changes only (minutes to hours)
- Maintain test coverage

**Tidying types**: Guard Clauses, Dead Code removal, Pattern normalization, Function extraction, Readability improvements

Works alongside the TDD Approach section's "Separate structural changes from behavioral changes" principle.
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": false,
Expand Down
18 changes: 16 additions & 2 deletions examples/javascript/common/src/kakao/send/send_bms.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
/**
* 카카오 브랜드 메시지 발송 예제
* 현재 targeting 타입 중 M, N의 경우는 카카오 측에서 인허가된 채널만 사용하실 수 있습니다.
* 카카오 브랜드 메시지(템플릿 기반) 발송 예제
* 이 파일은 templateId를 사용한 템플릿 기반 BMS 발송 예제입니다.
*
* BMS 자유형(템플릿 없이 직접 메시지 구성) 예제는 아래 파일들을 참고하세요:
* - send_bms_free_text.js: TEXT 타입 (텍스트 전용)
* - send_bms_free_text_with_buttons.js: TEXT 타입 + 버튼
* - send_bms_free_image.js: IMAGE 타입 (이미지 포함)
* - send_bms_free_image_with_buttons.js: IMAGE 타입 + 버튼
* - send_bms_free_wide.js: WIDE 타입 (와이드 이미지)
* - send_bms_free_wide_item_list.js: WIDE_ITEM_LIST 타입 (와이드 아이템 리스트)
* - send_bms_free_commerce.js: COMMERCE 타입 (상품 메시지)
* - send_bms_free_carousel_feed.js: CAROUSEL_FEED 타입 (캐러셀 피드)
* - send_bms_free_carousel_commerce.js: CAROUSEL_COMMERCE 타입 (캐러셀 커머스)
* - send_bms_free_premium_video.js: PREMIUM_VIDEO 타입 (프리미엄 비디오)
*
* targeting 타입 중 M, N의 경우는 카카오 측에서 인허가된 채널만 사용하실 수 있습니다.
* 그 외의 모든 채널은 I 타입만 사용 가능합니다.
*/
const {SolapiMessageService} = require('solapi');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* 카카오 BMS 자유형 CAROUSEL_COMMERCE 타입 발송 예제
* 캐러셀 커머스 형식으로, 여러 상품을 슬라이드로 보여주는 구조입니다.
* head + list(상품카드들) + tail 구조입니다.
* targeting 타입 중 M, N의 경우는 카카오 측에서 인허가된 채널만 사용하실 수 있습니다.
* 그 외의 모든 채널은 I 타입만 사용 가능합니다.
* 발신번호, 수신번호에 반드시 -, * 등 특수문자를 제거하여 기입하시기 바랍니다. 예) 01012345678
*/
const {SolapiMessageService} = require('solapi');
const messageService = new SolapiMessageService(
'ENTER_YOUR_API_KEY',
'ENTER_YOUR_API_SECRET',
);

// 단일 발송 예제
// imageId는 미리 업로드한 이미지 ID를 사용합니다.
// 이미지 업로드: messageService.uploadFile(filePath, 'KAKAO').then(res => res.fileId)
messageService
.sendOne({
to: '수신번호',
from: '계정에서 등록한 발신번호 입력',
type: 'BMS_FREE',
kakaoOptions: {
pfId: '연동한 비즈니스 채널의 pfId',
bms: {
targeting: 'I', // I: 전체, M/N: 인허가 채널만
chatBubbleType: 'CAROUSEL_COMMERCE',
carousel: {
// head: 캐러셀 상단 대표 이미지 및 설명 (선택)
head: {
header: '이번 주 베스트 상품',
content: '인기 상품을 만나보세요!',
imageId: '업로드한 헤드 이미지 ID',
linkMobile: 'https://m.example.com/best',
linkPc: 'https://example.com/best', // 선택
},
// list: 상품 카드 목록 (head 있으면 1-5개, 없으면 2-6개)
list: [
{
additionalContent: '무료배송', // 부가정보 (선택)
imageId: '업로드한 상품 이미지 ID',
coupon: {
title: '10% 할인 쿠폰',
description: '신규 회원 전용',
linkMobile: 'https://m.example.com/coupon1',
},
commerce: {
title: '상품명 1',
regularPrice: '30000',
discountPrice: '25000',
discountRate: '17',
},
buttons: [
{
linkType: 'WL',
name: '구매하기',
linkMobile: 'https://m.example.com/product1',
},
],
},
{
additionalContent: '오늘 출발',
imageId: '업로드한 상품 이미지 ID',
commerce: {
title: '상품명 2',
regularPrice: '50000',
discountPrice: '40000',
discountRate: '20',
},
buttons: [
{
linkType: 'WL',
name: '구매하기',
linkMobile: 'https://m.example.com/product2',
},
],
},
{
imageId: '업로드한 상품 이미지 ID',
commerce: {
title: '상품명 3',
regularPrice: '15000',
},
buttons: [
{
linkType: 'WL',
name: '구매하기',
linkMobile: 'https://m.example.com/product3',
},
],
},
],
// tail: 캐러셀 하단에 "더보기" 링크 (선택)
tail: {
linkMobile: 'https://m.example.com/all-products',
linkPc: 'https://example.com/all-products', // 선택
},
},
},
},
})
.then(res => console.log(res));

// head 없이 상품만 발송하는 예제
messageService
.sendOne({
to: '수신번호',
from: '계정에서 등록한 발신번호 입력',
type: 'BMS_FREE',
kakaoOptions: {
pfId: '연동한 비즈니스 채널의 pfId',
bms: {
targeting: 'I',
chatBubbleType: 'CAROUSEL_COMMERCE',
carousel: {
list: [
{
imageId: '업로드한 상품 이미지 ID',
commerce: {
title: '한정 특가 상품 A',
regularPrice: '100000',
discountPrice: '70000',
discountRate: '30',
},
buttons: [
{
linkType: 'WL',
name: '바로 구매',
linkMobile: 'https://m.example.com/productA',
},
],
},
{
imageId: '업로드한 상품 이미지 ID',
commerce: {
title: '한정 특가 상품 B',
regularPrice: '80000',
discountPrice: '60000',
discountRate: '25',
},
buttons: [
{
linkType: 'WL',
name: '바로 구매',
linkMobile: 'https://m.example.com/productB',
},
],
},
],
tail: {
linkMobile: 'https://m.example.com/sale',
},
},
},
},
})
.then(res => console.log(res));
Loading