Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
e23dc93
feat!: export all types/schemas and migrate to Effect
Palbahngmiyine Apr 8, 2026
7077d5e
Merge pull request #115 from Palbahngmiyine/feat/v6-effect-migration
Palbahngmiyine Apr 8, 2026
d46340c
ci: beta release-please 설정 수정 — versioning-strategy 및 버전 복원
Palbahngmiyine Apr 8, 2026
0285291
Merge pull request #118 from Palbahngmiyine/fix/beta-config
Palbahngmiyine Apr 8, 2026
24a12de
ci: versioning-strategy → versioning 속성명 수정
Palbahngmiyine Apr 8, 2026
8f192db
Merge pull request #120 from Palbahngmiyine/fix/beta-versioning
Palbahngmiyine Apr 8, 2026
b62dda1
ci: prerelease-type을 beta.0으로 변경하여 시리얼 넘버링 활성화
Palbahngmiyine Apr 8, 2026
f385a42
Merge pull request #123 from Palbahngmiyine/fix/prerelease-counter-beta
Palbahngmiyine Apr 8, 2026
0de676b
chore(beta): release solapi 6.0.0-beta.0
github-actions[bot] Apr 8, 2026
fcbdc40
Merge pull request #125 from solapi/release-please--branches--beta--c…
Palbahngmiyine Apr 8, 2026
b3223df
ci: biome formatter에서 package.json 제외
Palbahngmiyine Apr 8, 2026
8d85dc4
Merge pull request #126 from Palbahngmiyine/fix/biome-pkg-json
Palbahngmiyine Apr 8, 2026
71e5989
chore: .claude 에이전트 및 스킬 설정 복원
Palbahngmiyine Apr 14, 2026
0421bfc
chore: 사용하지 않는 .cursor/rules 디렉토리 삭제
Palbahngmiyine Apr 14, 2026
58b5abc
refactor: 레거시 클래스/수동 타입 제거 및 Effect Schema 전면 전환
Palbahngmiyine Apr 14, 2026
6e149ef
fix: handleServerErrorResponse null JSON 방어 및 코드 간결화
Palbahngmiyine Apr 14, 2026
e33df23
fix: handleClientErrorResponse에 동일한 null/비정형 JSON 방어 적용
Palbahngmiyine Apr 14, 2026
fc395c9
Merge pull request #5 from Palbahngmiyine/refactor/effect-migration-l…
Palbahngmiyine Apr 14, 2026
2589f11
refactor: CLAUDE.md 원칙 위반 정리 및 dead code 제거
Palbahngmiyine Apr 14, 2026
0d9d7b4
fix: 리뷰 피드백 반영 — isErrorResponse 강화, 에러 처리 일관성, examples 업데이트
Palbahngmiyine Apr 14, 2026
ac1206d
refactor: sendOne 전용 스키마/타입 및 barrel export 정리
Palbahngmiyine Apr 14, 2026
2247205
Merge pull request #6 from Palbahngmiyine/refactor/claude-md-violatio…
Palbahngmiyine Apr 14, 2026
556c729
refactor: dead code 제거, deprecated alias 정리, bindServices 명시적 바인딩 전환
Palbahngmiyine Apr 14, 2026
233bb6b
fix: 리뷰 피드백 반영 — 문서 업데이트, 테스트 보강
Palbahngmiyine Apr 14, 2026
1f3fc8a
fix: 테스트에 expect.assertions() 추가로 false-green 방지
Palbahngmiyine Apr 14, 2026
10527b1
Merge pull request #7 from Palbahngmiyine/refactor/dead-code-and-clau…
Palbahngmiyine Apr 14, 2026
5e0cb33
refactor: Effect Language Service 도입 및 타입 안전성·DRY 개선
Palbahngmiyine Apr 14, 2026
81ab581
Merge pull request #8 from Palbahngmiyine/refactor/effect-language-se…
Palbahngmiyine Apr 14, 2026
2faa1d1
refactor: 미사용 export 및 dead file 제거
Palbahngmiyine Apr 14, 2026
80708fb
Merge pull request #9 from Palbahngmiyine/refactor/remove-unused-exports
Palbahngmiyine Apr 14, 2026
1a2214c
refactor: Schema.transformOrFail 전환 및 불필요한 주석 제거
Palbahngmiyine Apr 15, 2026
3635405
fix: 리뷰 피드백 반영 — 주석 누락 제거 및 sendRequestConfigSchema 테스트 추가
Palbahngmiyine Apr 15, 2026
148df23
test: sendRequestConfigSchema 테스트 assertion 강화
Palbahngmiyine Apr 15, 2026
52e5e5e
test: encode round-trip 중복 assertion 제거
Palbahngmiyine Apr 15, 2026
bb93198
Merge pull request #10 from Palbahngmiyine/refactor/effect-schema-tra…
Palbahngmiyine Apr 15, 2026
de464e4
refactor: dead code 제거 및 타입 어설션 개선
Palbahngmiyine Apr 15, 2026
e8d5e9c
fix: restore default message schema export
Palbahngmiyine Apr 15, 2026
efe0d47
Merge pull request #11 from Palbahngmiyine/refactor/dead-code-and-typ…
Palbahngmiyine Apr 15, 2026
4672d2b
chore(ci): harden supply-chain workflows
Palbahngmiyine Apr 15, 2026
17abba4
fix(ci): use release-please component branch names
Palbahngmiyine Apr 15, 2026
0c93742
Merge pull request #140 from Palbahngmiyine/codex/release-please-bran…
Palbahngmiyine Apr 15, 2026
56708d6
Merge branch 'solapi:master' into master
Palbahngmiyine Apr 15, 2026
359ade5
Merge branch 'beta' into master
Palbahngmiyine Apr 15, 2026
6e4ff25
Merge pull request #141 from Palbahngmiyine/master
Palbahngmiyine Apr 15, 2026
c929ee8
feat(ci): unify release workflows for npm Trusted Publishers OIDC
Palbahngmiyine Apr 16, 2026
cde8d5f
Merge pull request #142 from Palbahngmiyine/ci/unified-release-workfl…
Palbahngmiyine Apr 16, 2026
9ea6c55
chore(beta): release solapi 6.0.0-beta.1
github-actions[bot] Apr 16, 2026
0a07696
Merge pull request #143 from solapi/release-please--branches--beta--c…
Palbahngmiyine Apr 16, 2026
cd41c51
fix(ci): use Node 24 for npm Trusted Publishers OIDC support
Palbahngmiyine Apr 16, 2026
49a55ed
Merge pull request #145 from Palbahngmiyine/fix/oidc-npm-version
Palbahngmiyine Apr 16, 2026
6ce5eaf
ci: trigger beta release with Node 24 OIDC publish
Palbahngmiyine Apr 16, 2026
2bfaa70
Merge pull request #147 from Palbahngmiyine/ci/trigger-beta-release
Palbahngmiyine Apr 16, 2026
5034366
chore(beta): release solapi 6.0.0-beta.2
github-actions[bot] Apr 16, 2026
4bbefb8
Merge pull request #148 from solapi/release-please--branches--beta--c…
Palbahngmiyine Apr 16, 2026
0042ae6
refactor: remove dead code and align with effect best practices
Palbahngmiyine Apr 16, 2026
8451c14
Merge pull request #150 from Palbahngmiyine/sync/refactor-to-beta
Palbahngmiyine Apr 16, 2026
3452bac
chore: trigger beta release for refactor changes
Palbahngmiyine Apr 17, 2026
d5cf2f7
Merge pull request #151 from Palbahngmiyine/ci/trigger-beta-release-v3
Palbahngmiyine Apr 17, 2026
6ef3982
docs(examples): drop stale sendOne note from send_sms comment
Palbahngmiyine Apr 17, 2026
92f6dfb
Merge pull request #152 from Palbahngmiyine/sync/beta-sendOne-cleanup
Palbahngmiyine Apr 17, 2026
6395aa7
Merge branch 'master' into beta
Palbahngmiyine Apr 17, 2026
7d2979c
feat(responses): sync query API schemas and add runtime validation
Palbahngmiyine Apr 17, 2026
61247e5
fix(responses): allow nullish startKey in kakao list responses
Palbahngmiyine Apr 17, 2026
90cd341
refactor(responses): address review findings for response validation
Palbahngmiyine Apr 17, 2026
5276053
refactor: tighten response decoding edge cases per review round 2
Palbahngmiyine Apr 17, 2026
d268c5e
fix(responses): accept new message types in countForCharge; null feat…
Palbahngmiyine Apr 17, 2026
ff37fe5
fix(errors): redact responseBody in production ResponseSchemaMismatch…
Palbahngmiyine Apr 17, 2026
300d9eb
fix(errors): redact all PII channels (validationErrors/url) in produc…
Palbahngmiyine Apr 17, 2026
0af8ead
fix(errors): safe-by-default redact gate; strip url fragment
Palbahngmiyine Apr 17, 2026
6df299c
refactor(stored-message): tighten option payloads to typed records
Palbahngmiyine Apr 17, 2026
28c912c
fix(statistics): keep dayPeriod.statusCode typed via partial MessageT…
Palbahngmiyine Apr 17, 2026
4e4317b
Merge pull request #154 from Palbahngmiyine/sync/response-schema-vali…
Palbahngmiyine Apr 17, 2026
0c197dd
chore(beta): release solapi 6.0.0-beta.3
github-actions[bot] Apr 17, 2026
bf74176
Merge pull request #155 from solapi/release-please--branches--beta--c…
Palbahngmiyine Apr 17, 2026
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
58 changes: 58 additions & 0 deletions .claude/agents/barrel-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
name: barrel-checker
description: src/ 하위 새 파일이 barrel export(index.ts)에 포함되었는지 검증하는 에이전트.
tools: Read, Grep, Glob, Bash
model: inherit
---

You are a barrel export consistency checker for the solapi-nodejs SDK.
v6.0.0에서 전체 타입 Export 방식을 채택했으며, barrel 패턴(index.ts re-export)을 유지해야 합니다.

## Export Structure

```
src/index.ts ← 최상위 entry point
├── src/errors/defaultError.ts ← 직접 export
├── src/models/index.ts ← barrel (base, requests, responses 통합)
│ ├── src/models/base/... ← 개별 파일을 models/index.ts에서 직접 re-export
│ ├── src/models/requests/index.ts ← 서브 barrel
│ └── src/models/responses/index.ts ← 서브 barrel
├── src/types/index.ts ← barrel (commonTypes.ts 등을 직접 re-export)
├── src/lib/... ← barrel 대상 아님 (내부 유틸리티)
└── src/services/... ← barrel 대상 아님 (SolapiMessageService에서 위임)
```

**검사 제외 대상**: `src/lib/`, `src/services/`는 barrel export 체인에 포함되지 않음.

## Check Process

1. `src/models/`, `src/types/`, `src/errors/` 하위의 모든 `.ts` 파일 수집 (`index.ts` 제외)
2. 모든 파일을 검사 대상으로 포함 (export가 없는 파일도 검사 — export 누락 자체가 문제일 수 있음)
3. 해당 파일이 적절한 barrel `index.ts`에서 re-export되는지 확인:
- `src/models/base/` 파일 → `src/models/index.ts`에서 직접 re-export (중간 index.ts 불필요)
- `src/models/requests/` 파일 → `src/models/requests/index.ts` → `src/models/index.ts`
- `src/models/responses/` 파일 → `src/models/responses/index.ts` → `src/models/index.ts`
- `src/models/base/kakao/bms/` 파일 → `bms/index.ts` → `src/models/index.ts`
- `src/types/` 파일 → `src/types/index.ts`에서 직접 re-export
- `src/errors/` 파일 → `src/index.ts`에서 직접 re-export (errors/index.ts 없음)
4. re-export 체인이 `src/index.ts`까지 연결되는지 확인

**중요**: 실제 barrel 구조를 먼저 읽어서 확인하세요. 중간 index.ts가 없는 디렉토리의 파일은 상위 barrel에서 직접 re-export됩니다.

## Export Pattern

```typescript
// Named re-export (권장)
export {
type KakaoButton,
kakaoButtonSchema,
} from './base/kakao/kakaoButton';

// Wildcard re-export (서브 barrel용)
export * from './requests/index';
```

## Report

누락된 export를 `파일 — barrel 위치`로 리포트하고, 추가할 export 코드를 제안.
export가 없는 파일은 별도로 경고 (의도적 private 파일인지 확인 필요).
51 changes: 51 additions & 0 deletions .claude/agents/effect-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: effect-reviewer
description: Effect 공식문서 원칙에 기반한 코드 리뷰 에이전트. 타입 안전 에러 처리, 의존성 주입, Schema 패턴 준수를 검증.
tools: Read, Grep, Glob, Bash
model: inherit
---

You are an Effect library pattern reviewer for the solapi-nodejs SDK.
All reviews MUST align with Effect official documentation (https://effect.website/docs/).
프로젝트 기본 규칙은 CLAUDE.md 참조. 이 문서는 Effect 특화 리뷰 항목만 기술합니다.

## Review Checklist

### A. 에러 처리

- Effect 경계를 벗어나는 `throw new Error(...)` → `Data.TaggedError` 사용 필수
- `Effect.tryPromise` 콜백 내부 throw는 `catch` 옵션으로 타입 에러 매핑 시에만 허용 (예: `defaultFetcher.ts`의 `catch` → `DefaultError`). `catch` 없으면 `UnknownException`이 되어 타입 안전성 상실
- Effect 코드 주변의 `try { ... } catch` → `Effect.catchTag`/`catchAll`/`catchTags`/`either` 사용 필수
- 주의: 비-Effect 코드(`fileToBase64.ts` 등)의 try-catch는 허용됨. Effect 파이프라인 내부만 검사
- 에러를 조용히 무시하는 패턴 → 반드시 명시적 처리 또는 타입 시스템 통한 전파
- `Effect.gen` 내부에서 throw 가능한 함수 호출 시:
- `JSON.parse`, `Schema.decodeUnknownSync` 등 → `Effect.try`로 래핑 필수
- `Schema.decodeUnknownEither`는 throw하지 않으므로 래핑 불필요
- `runSafePromise`에서 `Data.TaggedError`를 이중 래핑하지 않고 원본 그대로 전달

### B. 타입 안전성

- `any` 타입 → `unknown` + type guard 또는 Effect Schema
- `Error` 채널에 generic `Error` 사용 금지 → `Data.TaggedError` 기반 discriminated union

### C. Effect.gen 사용

- 단일 `yield*` Effect.gen → `flatMap`/`map`/`andThen`으로 간소화
- `function*` + `yield*` 사용 확인 (`yield` 아님)
- 참고: AGENTS.md에 `function* (_)` adapter 패턴이 문서화되어 있으나, 실제 코드베이스는 모두 adapter 없는 `function* ()` 사용. 새 코드는 adapter 없는 패턴 권장

### D. 의존성 주입 (테스트 코드 대상)

- `yield* ServiceTag` / `Layer.provide` 패턴은 `test/` 코드에서만 사용
- `src/services/`의 프로덕션 서비스는 class 기반(`DefaultService` 상속) — DI 규칙 적용 대상 아님
- 테스트에서 Requirements 타입이 모든 의존성을 union으로 추적하는지 확인

## Review Process

1. 대상 파일 목록 수집 (git diff 또는 지정 경로)
2. 각 파일에서 위 체크리스트 항목별 위반 검색
3. 위반 사항을 `파일:라인` 형식으로 보고, 공식문서 기반 수정 제안 포함

## Report Format

위반/경고/통과를 `파일:라인` 형식으로 분류하여 보고. 마지막에 `위반: N건 / 경고: N건 / 통과: N건` 요약 포함.
6 changes: 4 additions & 2 deletions .claude/agents/tidy-first.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ ALWAYS ask this question before adding features:
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`)
5. **Validate**: Run full validation (`pnpm lint && pnpm test && pnpm build`)
6. **Suggest Commit**: Propose commit message in Conventional Commits format

## Project Rules Compliance

Follow this project's code style:
Follow CLAUDE.md Core Principles and this project's code style:

- **Core Principles**: Zero Tolerance for Errors, Clarity over Cleverness, Conciseness, Reduce Comments, Read Before Writing
- **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`)
Expand All @@ -66,6 +67,7 @@ Follow this project's code style:
- **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
- **Test awareness**: Tidying 후 테스트가 성공/실패 경로를 모두 커버하는지 확인

## Commit Message Format

Expand Down
132 changes: 132 additions & 0 deletions .claude/skills/create-model/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: create-model
description: Effect Schema 기반 모델/요청 타입을 프로젝트 패턴에 맞게 스캐폴딩. barrel export, 테스트 파일 포함.
disable-model-invocation: true
---

# create-model

Effect Schema(https://effect.website/docs/schema/introduction/) 원칙에 따라 모델을 생성합니다.
프로젝트 검증 규칙은 CLAUDE.md "Mandatory Validation" 참조.

## Usage

```
/create-model <ModelName> [--type base|request|response] [--domain <domain>]
```

### 타입별 유효 도메인

| type | 유효 도메인 |
|------|-----------|
| base | messages, kakao, kakao/bms*, naver, rcs |

\* **kakao/bms 주의**: BMS 모델은 스키마 파일 + barrel export 외에 `src/models/base/kakao/kakaoOption.ts`의 `bmsChatBubbleTypeSchema`, `baseBmsSchema`, `BMS_REQUIRED_FIELDS`에도 통합이 필요합니다.
| request | common, iam, kakao, messages, voice |
| response | iam, kakao (또는 responses/ 루트에 직접 배치) |

```
# 예시
/create-model VoiceOption --type request --domain voice
```

## Step 1: 기존 패턴 확인

생성 전 반드시 동일 도메인의 기존 모델을 Read 도구로 읽어서 일관성을 유지합니다.

## Step 2: 모델 파일 생성

### Schema 정의 패턴

```typescript
import {Schema} from 'effect';

export const <modelName>Schema = Schema.Struct({
fieldName: Schema.String,
optionalField: Schema.optional(Schema.String),
// optional: 키 자체가 없을 수 있음 + NullOr: 값이 null일 수 있음
nullableField: Schema.optional(Schema.NullOr(Schema.String)),
status: Schema.Literal('ACTIVE', 'INACTIVE'),
});

export type <ModelName> = Schema.Schema.Type<typeof <modelName>Schema>;
```

### 네이밍 규칙

| 대상 | 패턴 | 예시 |
|------|------|------|
| Schema 변수 | camelCase + `Schema` 접미사 | `kakaoButtonSchema` |
| Type | PascalCase | `KakaoButton` |
| 파일명 | camelCase | `kakaoButton.ts` |

### Discriminated Union 패턴

```typescript
export const buttonSchema = Schema.Union(
webButtonSchema,
appButtonSchema,
);
```

### Transform 패턴

```typescript
// 주의: normalize 목적의 transform은 round-trip을 보장하지 않음
export const phoneSchema = Schema.String.pipe(
Schema.transform(Schema.String, {
decode: removeHyphens,
encode: s => s,
}),
Schema.filter(s => /^[0-9]+$/.test(s), {
message: () => '숫자만 포함해야 합니다.',
}),
);
```

## Step 3: Barrel Export 업데이트

barrel-checker 에이전트 규칙에 따라 가장 가까운 `index.ts`에 re-export 추가.
체인이 `src/index.ts`까지 연결되는지 확인.

```typescript
export {
type <ModelName>,
<modelName>Schema,
} from './<path>/<modelName>';
```

## Step 4: 테스트 파일 생성

`test/models/` 하위에 대응하는 테스트 파일:

```typescript
import {Schema} from 'effect';
import {describe, expect, it} from 'vitest';
import {<modelName>Schema} from '@models/<path>/<modelName>';

describe('<modelName>Schema', () => {
it('should decode valid input', () => {
const result = Schema.decodeUnknownEither(<modelName>Schema)({ /* valid */ });
expect(result._tag).toBe('Right');
});

it('should reject invalid input', () => {
const result = Schema.decodeUnknownEither(<modelName>Schema)({ /* invalid */ });
expect(result._tag).toBe('Left');
});

it.each([
['null field', { field: null }],
['empty string', { field: '' }],
['missing required', {}],
])('should handle edge case: %s', (_label, input) => {
const result = Schema.decodeUnknownEither(<modelName>Schema)(input);
// assert based on schema definition
});
});
```

## Step 5: 검증

CLAUDE.md "Mandatory Validation" 순서대로 `pnpm lint` → `pnpm test` → `pnpm build` 실행.
118 changes: 118 additions & 0 deletions .claude/skills/gen-e2e-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: gen-e2e-test
description: Effect 기반 E2E 테스트를 프로젝트 패턴(it.effect, Layer, Effect.either)에 맞게 생성. Effect 공식문서 원칙 준수.
disable-model-invocation: true
---

# gen-e2e-test

`@effect/vitest`의 `it.effect()` 패턴으로 E2E 테스트를 생성합니다.
Effect 공식문서: https://effect.website/docs/

## Usage

```
/gen-e2e-test <ServiceName> [--methods method1,method2]
```

## Step 1: 대상 서비스 분석

Read 도구로 서비스 구현과 기존 E2E 테스트를 읽습니다.

**중요**: 일부 서비스(cashService, iamService 등)는 plain vitest + async/await 패턴을 사용합니다. 기존 테스트가 있다면 해당 패턴을 따르고, 새로 작성하는 경우 아래 Effect 패턴(권장)을 사용합니다.

## Step 2: Layer 확인

`test/lib/test-layers.ts`에서 대상 서비스의 Layer 정의 확인.

### Layer가 없는 경우 — `test/lib/test-layers.ts`에 추가

`createServiceLayer`는 해당 파일 내부의 비공개 헬퍼입니다. 기존 정의 옆에 추가:

```typescript
export const <ServiceName>Tag = Context.GenericTag<<ServiceName>>('<ServiceName>');

export const <ServiceName>Live = createServiceLayer(
<ServiceName>Tag,
<ServiceName>,
);
```

## Step 3: E2E 테스트 생성

### Happy Path

```typescript
import {describe, expect, it} from '@effect/vitest';
import {Effect} from 'effect';

describe('<ServiceName> E2E', () => {
it.effect('should <동작 설명>', () =>
Effect.gen(function* () {
const service = yield* <ServiceName>Tag;

const result = yield* Effect.tryPromise(() =>
service.<methodName>(),
);

expect(result).toBeDefined();
}).pipe(Effect.provide(<ServiceName>Live)),
);
});
```

### Error Path — Effect.either

```typescript
it.effect('should handle <에러 상황> gracefully', () =>
Effect.gen(function* () {
const service = yield* <ServiceName>Tag;

const result = yield* Effect.either(
Effect.tryPromise(() =>
service.<methodName>(/* invalid args */),
),
);

expect(result._tag).toBe('Left');
if (result._tag === 'Left') {
// Effect.tryPromise는 UnknownException으로 래핑 — .error로 원본 에러 접근
expect(String(result.left.error)).toContain('예상되는 에러 메시지');
}
}).pipe(Effect.provide(<ServiceName>Live)),
);
```

### 병렬 호출

```typescript
// Effect.all은 기본 순차 실행. 병렬 실행 시 concurrency 옵션 필수
const [r1, r2] = yield* Effect.all([
Effect.tryPromise(() => service.method1()),
Effect.tryPromise(() => service.method2()),
], {concurrency: 'unbounded'});
```

### 환경변수

```typescript
// Effect.gen 내부에서 yield*로 사용
const sender = yield* Config.string('SOLAPI_SENDER').pipe(
Config.withDefault('01000000000'),
);
```

## Step 4: 검증

CLAUDE.md "Mandatory Validation" 순서대로 `pnpm lint` → `pnpm test` → `pnpm build` 실행.

## Checklist

기존 plain vitest 테스트를 확장하는 경우, 해당 파일의 기존 패턴을 따릅니다.
새로 작성하는 Effect 패턴 테스트의 경우:

- [ ] `@effect/vitest`에서 import (`vitest` 아님)
- [ ] `it.effect()` + `Effect.gen(function* () { ... })`
- [ ] `.pipe(Effect.provide(Layer))` 필수
- [ ] Happy path + Error path 모두 테스트
- [ ] `Effect.tryPromise` 에러는 `UnknownException` — `.error`로 원본 접근
Loading
Loading