Compare commits

..

3 Commits

Author SHA1 Message Date
geonhee-min
34c33202c6 issue #63
All checks were successful
Test CI / build (push) Successful in 1m16s
- 일정 목록 조회 기능 구현 중
- 일정 상세 조회 기능 구현 필요
2025-12-10 17:15:15 +09:00
geonhee-min
bb79557876 issue #65
- 일정 생성 기능 1차 구현
2025-12-10 17:14:09 +09:00
geonhee-min
9578b37c64 issue #
- 디렉토리 구조 개선
2025-12-10 17:13:28 +09:00
30 changed files with 352 additions and 76 deletions

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'drizzle-kit';
import dotenv from 'dotenv';
dotenv.config();
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
export default defineConfig({
dialect: "postgresql",

View File

@@ -20,7 +20,7 @@ export const userBlockingIdSeq = pgSequence("user_blocking_id_seq", { startWith
export const emailAddressIdSeq = pgSequence("email_address_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "9223372036854775807", cache: "1", cycle: false })
export const comment = pgTable("comment", {
id: uuid().primaryKey().notNull(),
id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),
content: text(),
createdAt: date("created_at"),
isDeleted: boolean("is_deleted").default(false),
@@ -72,19 +72,20 @@ export const participant = pgTable("participant", {
]);
export const schedule = pgTable("schedule", {
id: uuid().primaryKey().notNull(),
name: varchar(),
startDate: date("start_date"),
endDate: date("end_date"),
status: varchar(),
id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),
name: varchar().notNull(),
startDate: date("start_date").notNull(),
endDate: date("end_date").notNull(),
status: varchar().default('yet').notNull(),
content: text(),
isDeleted: boolean("is_deleted").default(false),
type: varchar(),
isDeleted: boolean("is_deleted").default(false).notNull(),
type: varchar().notNull(),
createdAt: date("created_at"),
owner: uuid(),
style: varchar(),
startTime: time("start_time"),
endTime: time("end_time"),
owner: uuid().notNull(),
style: varchar().notNull(),
startTime: time("start_time").notNull(),
endTime: time("end_time").notNull(),
dayList: varchar("day_list"),
}, (table) => [
index("schedule_enddatetime_idx").using("btree", table.endDate.asc().nullsLast().op("date_ops")),
index("schedule_name_idx").using("btree", table.name.asc().nullsLast().op("text_ops"), table.content.asc().nullsLast().op("text_ops")),

View File

@@ -6,9 +6,10 @@ import { RedisModule } from './redis/redis.module';
import { AccountModule } from './modules/account/account.module';
import { MailerModule } from './util/mailer/mailer.module';
import { AppConfigModule } from './config/config.module';
import { ScheduleModule } from './modules/schedule/schedule.module';
@Module({
imports: [AppConfigModule, DbModule, RedisModule, MailerModule, AccountModule],
imports: [AppConfigModule, DbModule, RedisModule, MailerModule, AccountModule, ScheduleModule],
controllers: [AppController],
providers: [AppService],
})

View File

@@ -5,6 +5,7 @@ import {
HttpException,
HttpStatus
} from '@nestjs/common';
import { JsonWebTokenError, TokenExpiredError } from '@nestjs/jwt';
import { FastifyReply, FastifyRequest } from 'fastify';
@Catch()
@@ -13,6 +14,39 @@ export class AllExceptionsFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse<FastifyReply>();
const request = ctx.getRequest<FastifyRequest>();
console.log(exception);
// TokenExpiredError
if (exception instanceof TokenExpiredError) {
const status = HttpStatus.UNAUTHORIZED;
const responseBody = {
statusCode: status,
message: 'Access Token Expired',
code: 'AccessTokenExpired',
timestamp: new Date().toISOString(),
path: ctx.getRequest().url
};
response.status(status).send(responseBody);
return;
}
// JsonWebTokenError
if (exception instanceof JsonWebTokenError) {
const status = HttpStatus.UNAUTHORIZED;
const responseBody = {
statusCode: status,
message: 'Invalid Token',
code: 'InvalidToken',
timestamp: new Date().toISOString(),
path: ctx.getRequest().url
};
response.status(status).send(responseBody);
return;
}
let status =
exception instanceof HttpException

View File

@@ -46,6 +46,6 @@ async function bootstrap() {
secret: process.env.JWT_SECRET
});
await app.listen(process.env.PORT ?? 3000, '0.0.0.0', () => { process.env.NODE_ENV !== 'prod' && console.log(`servier is running on ${process.env.PORT}`) });
// await app.listen(process.env.PORT || 3000, () => { process.env.NODE_ENV !== 'prod' && console.log(`service is running on ${process.env.PORT}`)});
}
bootstrap();

View File

@@ -6,7 +6,7 @@ export class AuthService {
constructor(private readonly jwtService: JwtService) {}
generateTokens(id: string) {
const accessToken = this.jwtService.sign({id: id}, { expiresIn: '5s' });
const accessToken = this.jwtService.sign({id: id}, { expiresIn: '1m' });
const refreshToken = this.jwtService.sign({id: id}, { expiresIn: '7d' });
return { accessToken, refreshToken };

View File

@@ -1,6 +1,6 @@
import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { TokenExpiredError } from "@nestjs/jwt";
import { JsonWebTokenError, TokenExpiredError } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_KEY } from "src/common/decorators/public.decorator";
@@ -23,23 +23,18 @@ export class JwtAccessAuthGuard extends AuthGuard('access-token') {
return super.canActivate(context);
}
handleRequest(err: any, user:any, info:any) {
if (info instanceof TokenExpiredError) {
throw new UnauthorizedException({
statusCode: 401,
message: 'Access Token Expired',
code: 'AccessTokenExpired'
});
}
handleRequest(err: any, user:any, info:any, context: ExecutionContext) {
if (err || !user) {
throw new UnauthorizedException({
statusCode: 401,
message: 'Invalid Token',
code: 'InvalidToken'
});
}
if (info instanceof TokenExpiredError) {
throw info;
}
if (info instanceof JsonWebTokenError) {
throw info;
}
throw err || new JsonWebTokenError('Unauthorized');
}
return user;
}
}

View File

@@ -2,10 +2,14 @@ import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { AccountRepo } from "src/modules/account/account.repo";
@Injectable()
export class JwtAccessStrategy extends PassportStrategy(Strategy, "access-token") {
constructor(configService: ConfigService) {
constructor(
configService: ConfigService,
private accountRepo: AccountRepo
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_SECRET')!
@@ -13,10 +17,11 @@ export class JwtAccessStrategy extends PassportStrategy(Strategy, "access-token"
}
async validate(payload: any) {
const token = ExtractJwt.fromAuthHeaderAsBearerToken();
if (!token) {
console.log(payload);
const user = await this.accountRepo.findById(payload.id);
if (!user) {
throw new UnauthorizedException();
}
return { id: payload.id };
return user;
}
}

View File

@@ -26,7 +26,7 @@ export class AccountRepo {
email: string,
password: string
) {
return this
return await this
.db
.insert(schema.account)
.values({
@@ -42,7 +42,7 @@ export class AccountRepo {
type: 'email' | 'accountId'
, id: string
) {
return this
return await this
.db
.select()
.from(schema.account)

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class CheckDuplicationResponseDto extends BaseResponseDto {
isDuplicated: boolean;

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class LoginResponseDto extends BaseResponseDto {
accessToken?: string;

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class RefreshAccessTokenResponseDto extends BaseResponseDto{
accessToken: string;

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class ResetPasswordResponseDto extends BaseResponseDto {

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class SendEmailVerificationCodeResponseDto extends BaseResponseDto{
}

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class SendResetPasswordCodeResponseDto extends BaseResponseDto {
}

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class SignupResponseDto extends BaseResponseDto {
}

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class VerifyEmailVerificationCodeResponseDto extends BaseResponseDto{
verified: boolean;

View File

@@ -1,4 +1,4 @@
import { BaseResponseDto } from "../base-response.dto";
import { BaseResponseDto } from "../../../../common/dto/base-response.dto";
export class VerifyResetPasswordCodeResponseDto extends BaseResponseDto {
verified: boolean;

View File

@@ -1,17 +1,14 @@
import { IsArray, IsDate, IsString } from '@nestjs/class-validator';
import { IsArray, IsDateString, IsString } from '@nestjs/class-validator';
export class CreateRequestDto {
@IsString()
name: string;
@IsDate()
startDate: Date;
@IsDateString()
startDate: string;
@IsDate()
endDate: Date;
@IsString()
status: string;
@IsDateString()
endDate: string;
@IsString()
content: string;

View File

@@ -0,0 +1,3 @@
import { BaseResponseDto } from "src/common/dto/base-response.dto";
export class CreateResponseDto extends BaseResponseDto {}

View File

@@ -0,0 +1,23 @@
import { BaseResponseDto } from "src/common/dto/base-response.dto";
class ScheduleDetail {
id: string;
name: string;
startDate: Date;
endDate: Date;
status: string;
content?: string | null;
isDeleted: boolean;
type: string;
createdAt: string | null;
owner: string;
style: string;
startTime: string;
endTime: string;
dayList?: string | null;
participantList?: string[] | null;
}
export class DetailResponseDto extends BaseResponseDto {
data?: ScheduleDetail | null;
}

View File

@@ -0,0 +1,7 @@
export { CreateRequestDto as CreateRequest } from './create/create-request.dto';
export { CreateResponseDto as CreateResponse } from './create/create-response.dto'
export { ListRequestDto as ListRequest } from './list/list-request.dto';
export { ListResponseDto as ListResponse } from './list/list-response.dto';
export { DetailResponseDto as DetailResponse } from './detail/detail-response.dto';

View File

@@ -0,0 +1,21 @@
import { IsArray, IsDateString, IsString } from "@nestjs/class-validator";
export class ListRequestDto {
@IsDateString()
startDate?: string;
@IsDateString()
endDate?: string;
@IsArray()
styleList?: string[];
@IsArray()
typeList?: string[];
@IsString()
status?: 'yet' | 'completed' | undefined;
@IsString()
name?: string;
}

View File

@@ -0,0 +1,15 @@
import { BaseResponseDto } from "src/common/dto/base-response.dto";
class ScheduleList {
name: string;
id: string;
startDate: Date;
endDate: Date;
type: string;
style: string;
status: string;
}
export class ListResponseDto extends BaseResponseDto {
data: ScheduleList[];
}

View File

@@ -0,0 +1,27 @@
import { Body, Controller, Get, Param, Post, Req, UseGuards } from "@nestjs/common";
import { JwtAccessAuthGuard } from "src/middleware/auth/guard/access-token.guard";
import { ScheduleService } from "./schedule.service";
import * as DTO from './dto';
@UseGuards(JwtAccessAuthGuard)
@Controller('schedule')
export class ScheduleController {
constructor(private readonly scheduleService: ScheduleService) {}
@Post('/')
async getList(@Req() req, @Body() data: DTO.ListRequest): Promise<DTO.ListResponse> {
const result = await this.scheduleService.getList(req.user.id, data);
return result;
}
@Get('/:id')
async getDetail(@Param('id') id: string): Promise<DTO.DetailResponse> {
const result = await this.scheduleService.getDetail(id);
return result;
}
@Post('/create')
async create(@Req() req, @Body() data: DTO.CreateRequest): Promise<DTO.CreateResponse> {
const result = await this.scheduleService.create(req.user.id, data);
return result;
}
}

View File

@@ -0,0 +1,13 @@
import { forwardRef, Module } from "@nestjs/common";
import { AuthModule } from "src/middleware/auth/auth.module";
import { ScheduleController } from "./schedule.controller";
import { ScheduleService } from "./schedule.service";
import { ScheduleRepo } from "./schedule.repo";
@Module({
imports: [forwardRef(() => AuthModule)],
controllers: [ScheduleController],
providers: [ScheduleService, ScheduleRepo],
exports: [ScheduleService, ScheduleRepo]
})
export class ScheduleModule {}

View File

@@ -1,43 +1,99 @@
import { Inject, Injectable } from '@nestjs/common';
import * as schema from 'drizzle/schema';
import { countDistinct, and, eq } from 'drizzle-orm';
import { countDistinct, and, eq, gt, gte, lte, like, inArray } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as DTO from './dto';
import { Converter } from 'src/util/converter';
@Injectable()
export class ScheduleRepo {
constructor(@Inject('DRIZZLE') private readonly db: NodePgDatabase<typeof schema>) {}
async getList(accountId: string) {
async getList(accountId: string, data: DTO.ListRequest) {
const { startDate, endDate, name, status, styleList, typeList } = data;
const schedule = schema.schedule;
const result = await this
.db
.select({
id: schedule.id,
name: schedule.name,
startDate: schedule.startDate,
endDate: schedule.endDate,
status: schedule.status,
style: schedule.style,
type: schedule.style,
})
.from(schedule)
.where(
and(
eq(schedule.owner, accountId),
startDate ? gte(schedule.startDate, Converter.formatDateToSqlDate(startDate)) : undefined,
endDate ? lte(schedule.endDate, Converter.formatDateToSqlDate(endDate)) : undefined,
name ? like(schedule.name, `%${name}%`) : undefined,
(typeList && typeList.length > 0) ? inArray(schedule.type, typeList) : undefined,
(styleList && styleList.length > 0) ? inArray(schedule.style, styleList) : undefined,
status ? eq(schedule.status, status) : undefined,
eq(schedule.isDeleted, false)
)
)
const resultData = result.map((schedule) => {
return {
id: schedule.id,
name: schedule.name,
type: schedule.type,
style: schedule.style,
status: schedule.status,
startDate: new Date(schedule.startDate),
endDate: new Date(schedule.endDate)
}
})
return resultData;
}
async getDetail(id: string) {
const schedule = schema.schedule;
const result = await this
.db
.select()
.from(schema.schedule)
.from(schedule)
.where(
and(
eq(schema.schedule.owner, accountId),
eq(schema.schedule.isDeleted, false)
eq(schedule.id, id),
eq(schedule.isDeleted, false)
)
);
return result;
}
async getDetail(id: string) {
const result = await this
async create(
accountId: string,
name: string,
startDate: string,
endDate: string,
startTime: string,
endTime: string,
style: string,
content: string,
type: string
) {
return await this
.db
.select()
.from(schema.schedule)
.where(
and(
eq(schema.schedule.id, id),
eq(schema.schedule.isDeleted, false)
)
);
return result[0];
}
async create() {
.insert(schema.schedule)
.values({
name: name,
content: content,
owner: accountId,
startDate: startDate,
endDate: endDate,
startTime: startTime,
endTime: endTime,
status: 'yet',
style: style,
type: type
});
}
}

View File

@@ -0,0 +1,69 @@
import { Injectable } from "@nestjs/common";
import { ScheduleRepo } from "./schedule.repo";
import * as DTO from './dto';
@Injectable()
export class ScheduleService {
constructor(
private readonly scheduleRepo: ScheduleRepo
) {}
async getList(accountId: string, data: DTO.ListRequest): Promise<DTO.ListResponse> {
const result = await this.scheduleRepo.getList(accountId, data);
return {
success: true,
data: result
};
}
async getDetail(id: string) {
const result = await this.scheduleRepo.getDetail(id);
if (result.length < 1) {
return {
success: false,
message: '존재하지 않는 일정입니다.'
};
}
const data = {
...result[0],
startDate: new Date(result[0].startDate),
endDate: new Date(result[0].endDate)
}
return {
success: true,
data: data
};
}
async create(accountId: string, data: DTO.CreateRequest): Promise<DTO.CreateResponse> {
const { name, content, startDate, endDate, startTime, endTime, style, type } = data;
const result = await this.scheduleRepo.create(
accountId,
name,
startDate,
endDate,
startTime,
endTime,
style,
content,
type
);
if (result.rowCount) {
return {
success: true,
message: "일정이 생성되었습니다."
};
} else {
return {
success: false,
message: "일정 생성에 실패하였습니다."
}
}
}
}

View File

@@ -8,4 +8,13 @@ export class Converter {
static comparePassword(rawPassword: string, hashedPassword: string) {
return bcrypt.compareSync(rawPassword, hashedPassword);
}
static formatDateToSqlDate(date: string): string {
const targetDate = new Date(date);
const year = targetDate.getUTCFullYear();
const month = (targetDate.getUTCMonth() + 1).toString().padStart(2, '0');
const day = (targetDate.getUTCDate()).toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
}