Skip to content

Commit 1a2214c

Browse files
refactor: Schema.transformOrFail 전환 및 불필요한 주석 제거
Effect 공식문서 원칙에 따라 throw 가능한 함수를 사용하는 Schema.transform을 Schema.transformOrFail로 전환하고, CLAUDE.md "코드가 자체 설명적이어야 함" 원칙에 따라 불필요한 what/how 주석을 제거한다. - kakaoOption: runSafeSync + Schema.transform → Schema.transformOrFail + Effect.mapError - requestConfig: formatWithTransfer + Schema.transform → safeFormatWithTransfer + Schema.transformOrFail - 10개 파일에서 불필요한 섹션 구분·단계별·설명 주석 ~35건 제거 (JSDoc API 문서 보존) - kakaoOption.test: performance.now() 비결정적 테스트 → 결정적 정확성 테스트 - stringifyQuery.test: @effect/vitest → vitest import (순수 유닛 테스트) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 80708fb commit 1a2214c

11 files changed

Lines changed: 28 additions & 62 deletions

File tree

src/index.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@ import MessageService from '@services/messages/messageService';
77
import StorageService from '@services/storage/storageService';
88
import {ApiKeyError} from './errors/defaultError';
99

10-
// Errors
1110
export * from './errors/defaultError';
12-
// Models (base types, request types, response types, schemas)
1311
export * from './models/index';
14-
// Common Types & Schemas
1512
export * from './types/index';
1613

1714
/**
@@ -22,14 +19,12 @@ export * from './types/index';
2219
* @see https://developers.solapi.com/category/nodejs
2320
*/
2421
export class SolapiMessageService {
25-
// CashService 위임
2622
/**
2723
* 잔액조회
2824
* @returns GetBalanceResponse
2925
*/
3026
readonly getBalance: typeof CashService.prototype.getBalance;
3127

32-
// IamService 위임
3328
/**
3429
* 080 수신 거부 조회
3530
* @param data 080 수신 거부 상세 조회용 request 데이터
@@ -51,7 +46,6 @@ export class SolapiMessageService {
5146
*/
5247
readonly getBlockNumbers: typeof IamService.prototype.getBlockNumbers;
5348

54-
// KakaoChannelService 위임
5549
/**
5650
* 카카오 채널 카테고리 조회
5751
*/
@@ -86,7 +80,6 @@ export class SolapiMessageService {
8680
*/
8781
readonly removeKakaoChannel: typeof KakaoChannelService.prototype.removeKakaoChannel;
8882

89-
// KakaoTemplateService 위임
9083
/**
9184
* 카카오 템플릿 카테고리 조회
9285
*/
@@ -137,7 +130,6 @@ export class SolapiMessageService {
137130
*/
138131
readonly removeKakaoAlimtalkTemplate: typeof KakaoTemplateService.prototype.removeKakaoAlimtalkTemplate;
139132

140-
// GroupService 위임
141133
/**
142134
* 그룹 생성
143135
* @param allowDuplicates 생성할 그룹이 중복 수신번호를 허용하는지 여부를 확인합니다.
@@ -206,7 +198,6 @@ export class SolapiMessageService {
206198
*/
207199
readonly removeGroup: typeof GroupService.prototype.removeGroup;
208200

209-
// MessageService 위임
210201
/**
211202
* 메시지 발송 기능, sendMany 함수보다 개선된 오류 표시 기능등을 제공합니다.
212203
* 한번의 요청으로 최대 10,000건까지 발송할 수 있습니다.
@@ -230,7 +221,6 @@ export class SolapiMessageService {
230221
*/
231222
readonly getStatistics: typeof MessageService.prototype.getStatistics;
232223

233-
// StorageService 위임
234224
/**
235225
* 파일(이미지) 업로드
236226
* 카카오 친구톡 이미지는 500kb, MMS는 200kb, 발신번호 서류 인증용 파일은 2mb의 제한이 있음
@@ -256,15 +246,12 @@ export class SolapiMessageService {
256246
const messageService = new MessageService(apiKey, apiSecret);
257247
const storageService = new StorageService(apiKey, apiSecret);
258248

259-
// CashService
260249
this.getBalance = cashService.getBalance.bind(cashService);
261250

262-
// IamService
263251
this.getBlacks = iamService.getBlacks.bind(iamService);
264252
this.getBlockGroups = iamService.getBlockGroups.bind(iamService);
265253
this.getBlockNumbers = iamService.getBlockNumbers.bind(iamService);
266254

267-
// KakaoChannelService
268255
this.getKakaoChannelCategories =
269256
kakaoChannelService.getKakaoChannelCategories.bind(kakaoChannelService);
270257
this.getKakaoChannels =
@@ -278,7 +265,6 @@ export class SolapiMessageService {
278265
this.removeKakaoChannel =
279266
kakaoChannelService.removeKakaoChannel.bind(kakaoChannelService);
280267

281-
// KakaoTemplateService
282268
this.getKakaoAlimtalkTemplateCategories =
283269
kakaoTemplateService.getKakaoAlimtalkTemplateCategories.bind(
284270
kakaoTemplateService,
@@ -308,7 +294,6 @@ export class SolapiMessageService {
308294
kakaoTemplateService,
309295
);
310296

311-
// GroupService
312297
this.createGroup = groupService.createGroup.bind(groupService);
313298
this.addMessagesToGroup =
314299
groupService.addMessagesToGroup.bind(groupService);
@@ -323,12 +308,10 @@ export class SolapiMessageService {
323308
groupService.removeGroupMessages.bind(groupService);
324309
this.removeGroup = groupService.removeGroup.bind(groupService);
325310

326-
// MessageService
327311
this.send = messageService.send.bind(messageService);
328312
this.getMessages = messageService.getMessages.bind(messageService);
329313
this.getStatistics = messageService.getStatistics.bind(messageService);
330314

331-
// StorageService
332315
this.uploadFile = storageService.uploadFile.bind(storageService);
333316
}
334317
}

src/lib/defaultFetcher.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ type DefaultRequest = {
1414
method: string;
1515
};
1616

17-
// Effect Data 타입으로 RetryableError 정의
1817
class RetryableError extends Data.TaggedError('RetryableError')<{
1918
readonly error?: unknown;
2019
}> {}

src/lib/effectErrorHandler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const runSafeSync = <E, A>(effect: Effect.Effect<A, E>): A => {
8686
});
8787
};
8888

89-
// Promise로 Effect 실행 — 예측된 실패는 원본 Effect 에러 그대로 reject
9089
export const runSafePromise = <E, A>(
9190
effect: Effect.Effect<A, E>,
9291
): Promise<A> => {

src/lib/fileToBase64.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {URL} from 'node:url';
33
import * as Effect from 'effect/Effect';
44
import {DefaultError} from '../errors/defaultError';
55

6-
// 내부 유틸: 주어진 문자열이 http(s) 스킴의 URL 인지 판별
76
const isHttpUrl = (value: string): boolean => {
87
try {
98
const url = new URL(value);
@@ -13,7 +12,6 @@ const isHttpUrl = (value: string): boolean => {
1312
}
1413
};
1514

16-
// URL → Base64 변환
1715
const fromUrl = (url: string) =>
1816
Effect.flatMap(
1917
Effect.tryPromise({
@@ -49,7 +47,6 @@ const fromUrl = (url: string) =>
4947
Effect.map(arrayBuffer => Buffer.from(arrayBuffer).toString('base64')),
5048
);
5149

52-
// 파일 경로 → Base64 변환
5350
const fromPath = (path: string) =>
5451
Effect.tryPromise({
5552
try: () => fs.readFile(path),

src/lib/stringifyQuery.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@ export default function stringifyQuery(
3535
return '';
3636
}
3737

38-
// 빈 객체인 경우 빈 문자열 반환 (쿼리 파라미터가 없으므로 접두사도 불필요)
3938
if (Object.keys(obj).length === 0) {
4039
return '';
4140
}
4241

43-
// 값 직렬화를 위한 내부 함수 (nested object 지원)
4442
const processValue = (key: string, value: unknown): string[] => {
4543
if (Array.isArray(value)) {
4644
if (options.indices === false) {
@@ -79,8 +77,6 @@ export default function stringifyQuery(
7977

8078
const queryString = pairs.join('&');
8179

82-
// 쿼리 스트링이 있으면 기본적으로 '?' 접두사를 붙임
83-
// addQueryPrefix가 명시적으로 false로 설정된 경우에만 접두사 없이 반환
8480
if (queryString) {
8581
return options.addQueryPrefix === false ? queryString : `?${queryString}`;
8682
}

src/models/base/kakao/kakaoOption.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import {runSafeSync} from '@lib/effectErrorHandler';
2-
import {Data, Effect, Array as EffectArray, pipe, Schema} from 'effect';
1+
import {
2+
Data,
3+
Effect,
4+
Array as EffectArray,
5+
ParseResult,
6+
pipe,
7+
Schema,
8+
} from 'effect';
39
import {
410
bmsButtonSchema,
511
bmsCarouselCommerceSchema,
@@ -12,7 +18,6 @@ import {
1218
} from './bms';
1319
import {kakaoButtonSchema} from './kakaoButton';
1420

15-
// Effect Data 타입을 활용한 에러 클래스
1621
export class VariableValidationError extends Data.TaggedError(
1722
'VariableValidationError',
1823
)<{
@@ -146,14 +151,12 @@ export type KakaoOptionBmsSchema = Schema.Schema.Type<
146151
const VARIABLE_KEY_PATTERN = /^#\{.+}$/;
147152
const DOT_PATTERN = /\./;
148153

149-
// Pure helper functions optimized with Effect
150154
const extractVariableName = (key: string): string =>
151155
VARIABLE_KEY_PATTERN.test(key) ? key.slice(2, -1) : key;
152156

153157
const formatVariableKey = (key: string): string =>
154158
VARIABLE_KEY_PATTERN.test(key) ? key : `#{${key}}`;
155159

156-
// Effect-based validation that returns Either instead of throwing
157160
export const validateVariableNames = (
158161
variables: Record<string, string>,
159162
): Effect.Effect<Record<string, string>, VariableValidationError> =>
@@ -167,7 +170,6 @@ export const validateVariableNames = (
167170
: Effect.succeed(variables),
168171
);
169172

170-
// Optimized transformation function using Effect pipeline
171173
export const transformVariables = (
172174
variables: Record<string, string>,
173175
): Effect.Effect<Record<string, string>, VariableValidationError> =>
@@ -189,14 +191,16 @@ export const baseKakaoOptionSchema = Schema.Struct({
189191
templateId: Schema.optional(Schema.String),
190192
variables: Schema.optional(
191193
Schema.Record({key: Schema.String, value: Schema.String}).pipe(
192-
Schema.transform(
194+
Schema.transformOrFail(
193195
Schema.Record({key: Schema.String, value: Schema.String}),
194196
{
195-
decode: fromU => {
196-
// runSafeSync를 사용하여 깔끔한 에러 메시지 제공
197-
return runSafeSync(transformVariables(fromU));
198-
},
199-
encode: toI => toI,
197+
decode: (fromU, _, ast) =>
198+
transformVariables(fromU).pipe(
199+
Effect.mapError(
200+
err => new ParseResult.Type(ast, fromU, err.message),
201+
),
202+
),
203+
encode: toI => ParseResult.succeed(toI),
200204
},
201205
),
202206
),

src/models/requests/messages/requestConfig.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
import {formatWithTransfer} from '@lib/stringDateTransfer';
2-
import {Schema} from 'effect';
1+
import {safeFormatWithTransfer} from '@lib/schemaUtils';
2+
import {Effect, ParseResult, Schema} from 'effect';
33
import pkg from '../../../../package.json';
44

5-
// SDK 및 OS 정보
65
export const osPlatform = `${process.platform} | ${process.version}`;
76
export const sdkVersion = `nodejs/${pkg.version}`;
87

9-
// Agent 정보 타입
108
export type DefaultAgentType = {
119
sdkVersion: string;
1210
osPlatform: string;
1311
appId?: string;
1412
};
1513

16-
// Agent 정보 Effect 스키마
1714
export const defaultAgentTypeSchema = Schema.Struct({
1815
sdkVersion: Schema.optional(Schema.String).pipe(
1916
Schema.withDecodingDefault(() => sdkVersion),
@@ -26,13 +23,17 @@ export const defaultAgentTypeSchema = Schema.Struct({
2623
appId: Schema.optional(Schema.String),
2724
});
2825

29-
// send 요청 시 사용되는 Config 스키마
3026
export const sendRequestConfigSchema = Schema.Struct({
3127
scheduledDate: Schema.optional(
3228
Schema.Union(Schema.DateFromSelf, Schema.DateFromString).pipe(
33-
Schema.transform(Schema.String, {
34-
decode: fromA => formatWithTransfer(fromA),
35-
encode: toI => new Date(toI),
29+
Schema.transformOrFail(Schema.String, {
30+
decode: (fromA, _, ast) =>
31+
safeFormatWithTransfer(fromA).pipe(
32+
Effect.mapError(
33+
err => new ParseResult.Type(ast, fromA, err.message),
34+
),
35+
),
36+
encode: toI => ParseResult.succeed(new Date(toI)),
3637
}),
3738
),
3839
),
@@ -45,7 +46,6 @@ export type SendRequestConfigSchema = Schema.Schema.Type<
4546
typeof sendRequestConfigSchema
4647
>;
4748

48-
// 메시지 요청 시 공통으로 사용하는 기본 스키마
4949
export const defaultMessageRequestSchema = Schema.Struct({
5050
allowDuplicates: Schema.optional(Schema.Boolean),
5151
agent: Schema.optional(defaultAgentTypeSchema),

src/models/requests/messages/sendMessage.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,15 @@ export const phoneNumberSchema = Schema.String.pipe(
99
decode: removeHyphens,
1010
encode: s => s,
1111
}),
12-
// 하이픈 제거 이후 값이 비어있지 않은지 확인 (예: "---" -> "")
1312
Schema.filter(s => s.trim().length > 0, {
1413
message: () => '전화번호는 빈 문자열일 수 없습니다.',
1514
}),
16-
// 숫자 및 하이픈만 허용하도록 강제. 하이픈 제거 후에는 숫자만 남아야 함
1715
Schema.filter(s => /^[0-9]+$/.test(s), {
1816
message: () =>
1917
'전화번호는 숫자 및 특수문자 - 외 문자를 포함할 수 없습니다.',
2018
}),
2119
);
2220

23-
// 빈 배열 검증을 위한 재사용 가능한 필터
2421
const nonEmptyArrayFilter = <A>(schema: Schema.Schema<A>) =>
2522
Schema.Array(schema).pipe(
2623
Schema.filter(arr => arr.length > 0, {
@@ -84,10 +81,8 @@ export type RequestSendMessagesSchema = Schema.Schema.Type<
8481
typeof requestSendMessageSchema
8582
>;
8683

87-
// 기본 Agent 객체 (sdkVersion, osPlatform 값 포함) – 빈 객체 디코딩으로 생성
8884
const defaultAgentValue = Schema.decodeSync(defaultAgentTypeSchema)({});
8985

90-
// Agent 스키마의 재사용 가능한 정의
9186
const agentWithDefaultSchema = Schema.optional(defaultAgentTypeSchema).pipe(
9287
Schema.withDecodingDefault(() => defaultAgentValue),
9388
Schema.withConstructorDefault(() => defaultAgentValue),

src/services/messages/messageService.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,11 @@ export default class MessageService extends DefaultService {
4949

5050
return runSafePromise(
5151
Effect.gen(function* () {
52-
// 1. 스키마 검증
5352
const messageSchema = yield* decodeWithBadRequest(
5453
requestSendMessageSchema,
5554
messages,
5655
);
5756

58-
// 2. MessageParameter -> Message 변환 및 기본 검증
5957
const messageParameters = Array.isArray(messageSchema)
6058
? messageSchema
6159
: [messageSchema];
@@ -84,7 +82,6 @@ export default class MessageService extends DefaultService {
8482
parameterObject,
8583
);
8684

87-
// 3. API 호출
8885
const response = yield* reqEffect<
8986
MultipleMessageSendingRequestSchema,
9087
DetailGroupMessageResponse
@@ -94,7 +91,6 @@ export default class MessageService extends DefaultService {
9491
body: parameter,
9592
});
9693

97-
// 4. 모든 메시지 발송건이 실패인 경우 MessageNotReceivedError 반환
9894
const {count} = response.groupInfo;
9995
const failedAll =
10096
response.failedMessageList.length > 0 &&

test/lib/stringifyQuery.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {describe, expect, it} from '@effect/vitest';
1+
import {describe, expect, it} from 'vitest';
22
import stringifyQuery from '@/lib/stringifyQuery';
33

44
describe('stringifyQuery', () => {

0 commit comments

Comments
 (0)