Skip to content
Merged
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
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
## Coding behavior

이 저장소의 모든 작업에 적용되는 행동 규칙. 속도보다 신중함에 가중을 둠.
사소한 작업은 판단에 맡김.

### 1. Think before coding

- 가정은 명시적으로 드러내라. 불확실하면 물어라.
- 해석이 여러 개면 모두 제시하라. 침묵하고 고르지 말 것.
- 더 단순한 접근이 보이면 말하라. 필요하면 사용자 의도에 반대하라.

### 2. Simplicity first

- 문제를 푸는 최소한의 코드만.
- 요청하지 않은 기능/추상화/에러 처리 금지.
- 200줄을 50줄로 줄일 수 있으면 다시 써라.

### 3. Surgical changes

- 작업이 요구하는 것만 건드려라.
- 인접한 코드/주석/포맷을 "개선"하지 마라.
- 기존 스타일을 따라라. 무관한 dead code는 언급만, 삭제는 하지 마라.
- 본인 변경이 만든 고아 import/변수만 제거하라.

### 4. Goal-driven execution

- 모든 작업을 검증 가능한 성공 기준으로 번역한 후 코딩 시작.
- "validation 추가" → "잘못된 입력에 대한 테스트가 통과한다."
- "버그 수정" → "재현 테스트가 통과한다."
- "X 리팩터" → "리팩터 전후 테스트가 모두 통과한다."

## Skill routing

When the user's request matches an available skill, ALWAYS invoke it using the Skill
Expand Down
5 changes: 4 additions & 1 deletion apps/api-server/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
volumes:
- ../../:/app
- /app/node_modules
- ${LOG_DIR:-../../logs}/api-server:/app/logs
- api_server_logs:/app/logs
expose:
- '3000'
depends_on:
Expand All @@ -35,3 +35,6 @@ services:
networks:
coin-net:
name: coin-net

volumes:
api_server_logs:
2 changes: 1 addition & 1 deletion apps/api-server/e2e/orders.e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('Orders E2E', () => {
method: 'POST',
cookies,
body: JSON.stringify({
exchange: 'upbit',
exchange: 'binance',
symbol: 'KRW-BTC',
side: 'buy',
type: 'market',
Expand Down
97 changes: 0 additions & 97 deletions apps/api-server/e2e/strategies.e2e-test.ts

This file was deleted.

40 changes: 3 additions & 37 deletions apps/api-server/src/activity/activity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PrismaService } from '../prisma/prisma.service';

export interface ActivityItem {
id: string;
type: 'order' | 'strategy_signal' | 'strategy_order' | 'risk_blocked' | 'login';
type: 'order' | 'login';
title: string;
description: string;
exchange?: string;
Expand All @@ -25,8 +25,7 @@ export class ActivityService {
): Promise<{ items: ActivityItem[]; nextCursor: string | null }> {
const cursorDate = cursor ? new Date(cursor) : undefined;

// Fetch from all 3 sources in parallel
const [orders, strategyLogs, logins] = await Promise.all([
const [orders, logins] = await Promise.all([
this.prisma.order.findMany({
where: {
userId,
Expand All @@ -35,15 +34,6 @@ export class ActivityService {
orderBy: { createdAt: 'desc' },
take: limit + 1,
}),
this.prisma.strategyLog.findMany({
where: {
strategy: { userId },
...(cursorDate ? { createdAt: { lt: cursorDate } } : {}),
},
include: { strategy: { select: { name: true, exchange: true, symbol: true, id: true } } },
orderBy: { createdAt: 'desc' },
take: limit + 1,
}),
this.prisma.loginHistory.findMany({
where: {
userId,
Expand All @@ -54,7 +44,6 @@ export class ActivityService {
}),
]);

// Map to unified type
const orderItems: ActivityItem[] = orders.map((o) => ({
id: `order-${o.id}`,
type: 'order' as const,
Expand All @@ -68,28 +57,6 @@ export class ActivityService {
createdAt: o.createdAt,
}));

const strategyItems: ActivityItem[] = strategyLogs.map((log) => {
const details = log.details as Record<string, unknown>;
const action = log.action;
let type: ActivityItem['type'] = 'strategy_signal';
if (action === 'order_placed') type = 'strategy_order';
if (action === 'risk_blocked') type = 'risk_blocked';

return {
id: `strategy-${log.id}`,
type,
title: `${log.strategy.name} — ${action.replace('_', ' ')}`,
description: log.signal
? `${log.signal.toUpperCase()} @ ${details.price || ''} (${details.reason || ''})`
: String(details.reason || details.error || ''),
exchange: log.strategy.exchange,
symbol: log.strategy.symbol,
side: log.signal || undefined,
link: `/strategies/${log.strategy.id}`,
createdAt: log.createdAt,
};
});

const loginItems: ActivityItem[] = logins.map((l) => ({
id: `login-${l.id}`,
type: 'login' as const,
Expand All @@ -100,8 +67,7 @@ export class ActivityService {
createdAt: l.createdAt,
}));

// Merge sort by createdAt desc
const all = [...orderItems, ...strategyItems, ...loginItems].sort(
const all = [...orderItems, ...loginItems].sort(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
);

Expand Down
4 changes: 0 additions & 4 deletions apps/api-server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { ExchangeKeysModule } from './exchange-keys/exchange-keys.module';
import { OrdersModule } from './orders/orders.module';
import { StrategiesModule } from './strategies/strategies.module';
import { NotificationsModule } from './notifications/notifications.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { ActivityModule } from './activity/activity.module';
import { FlowsModule } from './flows/flows.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';

@Module({
Expand Down Expand Up @@ -47,11 +45,9 @@ import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
MarketsModule,
ExchangeKeysModule,
OrdersModule,
StrategiesModule,
NotificationsModule,
PortfolioModule,
ActivityModule,
FlowsModule,
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ describe('CreateExchangeKeyHandler', () => {
it('암호화된 자격증명으로 거래소 키를 생성/upsert해야 한다', async () => {
mockPrisma.exchangeKey.upsert.mockResolvedValue({
id: 'key-1',
exchange: 'upbit',
exchange: 'binance',
createdAt: new Date(),
});

const result = await handler.execute(
new CreateExchangeKeyCommand('user-1', {
exchange: 'upbit',
exchange: 'binance',
apiKey: 'my-api-key',
secretKey: 'my-secret',
} as never),
Expand All @@ -65,7 +65,7 @@ describe('CreateExchangeKeyHandler', () => {
await expect(
handler.execute(
new CreateExchangeKeyCommand('user-1', {
exchange: 'upbit',
exchange: 'binance',
apiKey: 'key',
secretKey: 'secret',
} as never),
Expand All @@ -79,7 +79,7 @@ describe('CreateExchangeKeyHandler', () => {
await expect(
handler.execute(
new CreateExchangeKeyCommand('user-1', {
exchange: 'upbit',
exchange: 'binance',
apiKey: 'bad-key',
secretKey: 'bad-secret',
} as never),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../prisma/prisma.service';
import { encrypt } from '@coin/utils';
import { UpbitRest, BinanceRest, BybitRest, IExchangeRest } from '@coin/exchange-adapters';
import { BinanceRest, IExchangeRest } from '@coin/exchange-adapters';
import type { ExchangeId, ExchangeCredentials } from '@coin/types';
import { CreateExchangeKeyCommand } from './create-exchange-key.command';

const REST_ADAPTERS: Record<ExchangeId, () => IExchangeRest> = {
upbit: () => new UpbitRest(),
binance: () => new BinanceRest(),
bybit: () => new BybitRest(),
};

@CommandHandler(CreateExchangeKeyCommand)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export class CreateExchangeKeyDto {
@ApiProperty({
description: '대상 거래소',
example: 'binance',
enum: ['upbit', 'binance', 'bybit'],
enum: ['binance'],
})
@IsIn(['upbit', 'binance', 'bybit'])
@IsIn(['binance'])
exchange!: string;

@ApiProperty({ description: '거래소 API 키', example: 'aB3dEfGhIjKlMnOpQrStUvWxYz012345' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../prisma/prisma.service';
import { decrypt } from '@coin/utils';
import { UpbitRest, BinanceRest, BybitRest, IExchangeRest } from '@coin/exchange-adapters';
import { BinanceRest, IExchangeRest } from '@coin/exchange-adapters';
import type { ExchangeId, ExchangeCredentials } from '@coin/types';
import { GetBalancesQuery } from './get-balances.query';

const REST_ADAPTERS: Record<ExchangeId, () => IExchangeRest> = {
upbit: () => new UpbitRest(),
binance: () => new BinanceRest(),
bybit: () => new BybitRest(),
};

@QueryHandler(GetBalancesQuery)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('GetExchangeKeysHandler', () => {
});

it('민감한 데이터 없이 거래소 키를 반환해야 한다', async () => {
const keys = [{ id: 'key-1', exchange: 'upbit', createdAt: new Date() }];
const keys = [{ id: 'key-1', exchange: 'binance', createdAt: new Date() }];
mockPrisma.exchangeKey.findMany.mockResolvedValue(keys);

const result = await handler.execute(new GetExchangeKeysQuery('user-1'));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { NotFoundException } from '@nestjs/common';
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { PrismaService } from '../../prisma/prisma.service';
import { UpbitRest, BinanceRest, BybitRest, IExchangeRest } from '@coin/exchange-adapters';
import { BinanceRest, IExchangeRest } from '@coin/exchange-adapters';
import type { ExchangeId } from '@coin/types';
import { GetMarketsQuery } from './get-markets.query';

const REST_ADAPTERS: Record<ExchangeId, () => IExchangeRest> = {
upbit: () => new UpbitRest(),
binance: () => new BinanceRest(),
bybit: () => new BybitRest(),
};

@QueryHandler(GetMarketsQuery)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../../prisma/prisma.service';
import { decrypt } from '@coin/utils';
import { UpbitRest, BinanceRest, BybitRest, IExchangeRest } from '@coin/exchange-adapters';
import { BinanceRest, IExchangeRest } from '@coin/exchange-adapters';
import type { ExchangeId, ExchangeCredentials } from '@coin/types';
import { GetOpenOrdersQuery } from './get-open-orders.query';

const REST_ADAPTERS: Record<ExchangeId, () => IExchangeRest> = {
upbit: () => new UpbitRest(),
binance: () => new BinanceRest(),
bybit: () => new BybitRest(),
};

@QueryHandler(GetOpenOrdersQuery)
Expand Down
8 changes: 0 additions & 8 deletions apps/api-server/src/flows/commands/create-flow.command.ts

This file was deleted.

Loading
Loading