Skip to content

Commit 5938791

Browse files
sscoderaticlaudeCopilot
authored
[기능추가] [3-4] Schedule CRUD API 구현 (#105)
* [기능추가] [3-4] Schedule CRUD API 구현 (#49) - GET /api/ledgers/:id/schedules — 반복 일정 목록 조회 - POST /api/ledgers/:id/schedules — 일정 추가 - PATCH /api/ledgers/:id/schedules/:scheduleId — 일정 수정 - DELETE /api/ledgers/:id/schedules/:scheduleId — 소프트 삭제 - GET /api/ledgers/:id/schedules/upcoming?date= — 예정 거래 목록 조회 - 주말 처리 로직: FORWARD → 직전 금요일, BACKWARD → 다음 월요일 - 31일 설정 시 짧은 달 말일 자동 조정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update 메서드에 빈 payload 가드 추가 (P2 피드백 반영) Agent-Logs-Url: https://github.com/sscoderati/marrylife/sessions/40a3c5a3-a61a-46a9-85f1-93c991c679ab Co-authored-by: sscoderati <69716992+sscoderati@users.noreply.github.com> * fix: UTC 기반 날짜 처리 통일 및 update 메서드 리소스 검증 추가 Agent-Logs-Url: https://github.com/sscoderati/marrylife/sessions/4d46778f-aa88-45dd-9266-f6f1f9fe9f30 Co-authored-by: sscoderati <69716992+sscoderati@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 23b0929 commit 5938791

File tree

7 files changed

+481
-0
lines changed

7 files changed

+481
-0
lines changed

apps/api/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AccountsModule } from './accounts/accounts.module';
1010
import { TransactionsModule } from './transactions/transactions.module';
1111
import { ReportsModule } from './reports/reports.module';
1212
import { BudgetsModule } from './budgets/budgets.module';
13+
import { SchedulesModule } from './schedules/schedules.module';
1314

1415
@Module({
1516
imports: [
@@ -22,6 +23,7 @@ import { BudgetsModule } from './budgets/budgets.module';
2223
TransactionsModule,
2324
ReportsModule,
2425
BudgetsModule,
26+
SchedulesModule,
2527
],
2628
controllers: [AppController],
2729
providers: [AppService],
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsEnum, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
3+
4+
export class CreateScheduleDto {
5+
@ApiProperty({ example: '월급날' })
6+
@IsString()
7+
@MaxLength(100)
8+
name!: string;
9+
10+
@ApiProperty({ example: 25, description: '매월 반복 일자 (1~31)' })
11+
@IsInt()
12+
@Min(1)
13+
@Max(31)
14+
dayOfMonth!: number;
15+
16+
@ApiPropertyOptional({ enum: ['FORWARD', 'BACKWARD'], description: '주말 처리 방식' })
17+
@IsOptional()
18+
@IsEnum(['FORWARD', 'BACKWARD'])
19+
onWeekends?: 'FORWARD' | 'BACKWARD';
20+
21+
@ApiPropertyOptional({
22+
enum: ['INCOME', 'EXPENSE', 'TRANSFER', 'DEBT_PAYMENT', 'SAVING'],
23+
description: '거래 템플릿 타입 (null이면 단순 메모 일정)',
24+
})
25+
@IsOptional()
26+
@IsEnum(['INCOME', 'EXPENSE', 'TRANSFER', 'DEBT_PAYMENT', 'SAVING'])
27+
templateType?: 'INCOME' | 'EXPENSE' | 'TRANSFER' | 'DEBT_PAYMENT' | 'SAVING';
28+
29+
@ApiPropertyOptional({ example: 300000 })
30+
@IsOptional()
31+
@IsInt()
32+
@Min(1)
33+
templateAmount?: number;
34+
35+
@ApiPropertyOptional({ example: 1 })
36+
@IsOptional()
37+
@IsInt()
38+
templateCategoryId?: number;
39+
40+
@ApiPropertyOptional({ example: 2 })
41+
@IsOptional()
42+
@IsInt()
43+
templateAccountInId?: number;
44+
45+
@ApiPropertyOptional({ example: 3 })
46+
@IsOptional()
47+
@IsInt()
48+
templateAccountOutId?: number;
49+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsDateString } from 'class-validator';
3+
4+
export class QueryUpcomingSchedulesDto {
5+
@ApiProperty({ example: '2026-03-01', description: '기준 날짜 (YYYY-MM-DD)' })
6+
@IsDateString()
7+
date!: string;
8+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ApiPropertyOptional } from '@nestjs/swagger';
2+
import { IsEnum, IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';
3+
4+
export class UpdateScheduleDto {
5+
@ApiPropertyOptional({ example: '월급날' })
6+
@IsOptional()
7+
@IsString()
8+
@MaxLength(100)
9+
name?: string;
10+
11+
@ApiPropertyOptional({ example: 25 })
12+
@IsOptional()
13+
@IsInt()
14+
@Min(1)
15+
@Max(31)
16+
dayOfMonth?: number;
17+
18+
@ApiPropertyOptional({ enum: ['FORWARD', 'BACKWARD'], nullable: true })
19+
@IsOptional()
20+
@IsEnum(['FORWARD', 'BACKWARD'])
21+
onWeekends?: 'FORWARD' | 'BACKWARD' | null;
22+
23+
@ApiPropertyOptional({
24+
enum: ['INCOME', 'EXPENSE', 'TRANSFER', 'DEBT_PAYMENT', 'SAVING'],
25+
nullable: true,
26+
})
27+
@IsOptional()
28+
@IsEnum(['INCOME', 'EXPENSE', 'TRANSFER', 'DEBT_PAYMENT', 'SAVING'])
29+
templateType?: 'INCOME' | 'EXPENSE' | 'TRANSFER' | 'DEBT_PAYMENT' | 'SAVING' | null;
30+
31+
@ApiPropertyOptional({ example: 300000, nullable: true })
32+
@IsOptional()
33+
@IsInt()
34+
@Min(1)
35+
templateAmount?: number | null;
36+
37+
@ApiPropertyOptional({ example: 1, nullable: true })
38+
@IsOptional()
39+
@IsInt()
40+
templateCategoryId?: number | null;
41+
42+
@ApiPropertyOptional({ example: 2, nullable: true })
43+
@IsOptional()
44+
@IsInt()
45+
templateAccountInId?: number | null;
46+
47+
@ApiPropertyOptional({ example: 3, nullable: true })
48+
@IsOptional()
49+
@IsInt()
50+
templateAccountOutId?: number | null;
51+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
Body,
3+
Controller,
4+
Delete,
5+
Get,
6+
Param,
7+
ParseIntPipe,
8+
Patch,
9+
Post,
10+
Query,
11+
UseGuards,
12+
} from '@nestjs/common';
13+
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
14+
import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator';
15+
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
16+
import { LedgerMemberGuard } from '../common/guards/ledger-member.guard';
17+
import { CreateScheduleDto } from './dto/create-schedule.dto';
18+
import { QueryUpcomingSchedulesDto } from './dto/query-upcoming-schedules.dto';
19+
import { UpdateScheduleDto } from './dto/update-schedule.dto';
20+
import { SchedulesService } from './schedules.service';
21+
22+
@ApiTags('schedules')
23+
@ApiBearerAuth()
24+
@Controller('ledgers/:ledgerId/schedules')
25+
@UseGuards(JwtAuthGuard, LedgerMemberGuard)
26+
export class SchedulesController {
27+
constructor(private readonly schedulesService: SchedulesService) {}
28+
29+
@Get()
30+
@ApiOperation({ summary: '반복 일정 목록 조회' })
31+
async findAll(
32+
@CurrentUser() user: AuthUser,
33+
@Param('ledgerId', ParseIntPipe) ledgerId: number,
34+
) {
35+
return this.schedulesService.findAll(ledgerId, user.userId);
36+
}
37+
38+
@Get('upcoming')
39+
@ApiOperation({ summary: '예정 거래 목록 조회' })
40+
async findUpcoming(
41+
@CurrentUser() user: AuthUser,
42+
@Param('ledgerId', ParseIntPipe) ledgerId: number,
43+
@Query() query: QueryUpcomingSchedulesDto,
44+
) {
45+
return this.schedulesService.findUpcoming(ledgerId, user.userId, query);
46+
}
47+
48+
@Post()
49+
@ApiOperation({ summary: '반복 일정 추가' })
50+
async create(
51+
@CurrentUser() user: AuthUser,
52+
@Param('ledgerId', ParseIntPipe) ledgerId: number,
53+
@Body() dto: CreateScheduleDto,
54+
) {
55+
return this.schedulesService.create(ledgerId, user.userId, dto);
56+
}
57+
58+
@Patch(':scheduleId')
59+
@ApiOperation({ summary: '반복 일정 수정' })
60+
async update(
61+
@CurrentUser() user: AuthUser,
62+
@Param('ledgerId', ParseIntPipe) ledgerId: number,
63+
@Param('scheduleId', ParseIntPipe) scheduleId: number,
64+
@Body() dto: UpdateScheduleDto,
65+
) {
66+
return this.schedulesService.update(ledgerId, scheduleId, user.userId, dto);
67+
}
68+
69+
@Delete(':scheduleId')
70+
@ApiOperation({ summary: '반복 일정 소프트 삭제' })
71+
async remove(
72+
@CurrentUser() user: AuthUser,
73+
@Param('ledgerId', ParseIntPipe) ledgerId: number,
74+
@Param('scheduleId', ParseIntPipe) scheduleId: number,
75+
) {
76+
return this.schedulesService.remove(ledgerId, scheduleId, user.userId);
77+
}
78+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { JwtModule } from '@nestjs/jwt';
3+
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
4+
import { LedgerMemberGuard } from '../common/guards/ledger-member.guard';
5+
import { SchedulesController } from './schedules.controller';
6+
import { SchedulesService } from './schedules.service';
7+
8+
@Module({
9+
imports: [JwtModule.register({})],
10+
controllers: [SchedulesController],
11+
providers: [SchedulesService, JwtAuthGuard, LedgerMemberGuard],
12+
})
13+
export class SchedulesModule {}

0 commit comments

Comments
 (0)