Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d801d2a
feat: article에 organizationId 추가
liveforpresent Nov 15, 2025
8352e26
fix: 게시글 생성 시 organizationId 사용하도록 수정
liveforpresent Nov 15, 2025
abd70e2
feat: 게시글 수정 시 organizationId 사용하도록 수정
liveforpresent Nov 15, 2025
c5fccaf
fix: 게시글 삭제 시 roganizationId 사용하도록 수정
liveforpresent Nov 15, 2025
28fcbe3
feat: organization 전용 article controller 생성 및 guard 사용
liveforpresent Nov 15, 2025
74dc9b1
feat: Role type 구현
liveforpresent Nov 15, 2025
f1c14cd
fix: JwtPayload 범용성을 위해 userId -> sub으로 수정
liveforpresent Nov 15, 2025
4a87809
fix: 토큰 발급 시 Role을 포함하도록 수정
liveforpresent Nov 15, 2025
c689d8d
fix: 유저 생성 이벤트 수정
liveforpresent Nov 15, 2025
aecd570
feat: organization decorator 구현 및 적용
liveforpresent Nov 15, 2025
3b4388d
feat: role decorator 구현
liveforpresent Nov 15, 2025
577a3f5
feat: role guard 구현
liveforpresent Nov 15, 2025
85110ef
feat: organization role guard 적용
liveforpresent Nov 15, 2025
ef09a4c
feat: user role guard 적용
liveforpresent Nov 15, 2025
6f247ac
feat: article 조회 모델 구현
liveforpresent Nov 15, 2025
80e86ac
feat: organization article repository 구현
liveforpresent Nov 15, 2025
c4e6560
feat: 기관 전용 게시물 조회 구현
liveforpresent Nov 15, 2025
2fece12
feat: 기관 전용 게시글 조회 api 구현
liveforpresent Nov 15, 2025
c40f474
docs: organization article swagger 문서 작성
liveforpresent Nov 15, 2025
5048062
fix: 기관 게시글 목록 조회 시, 신청 시작/마감 일시 표시되도록 수정
liveforpresent Nov 16, 2025
551fcbc
fix: copilot 리뷰 수정사항 반영
liveforpresent Nov 16, 2025
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
1 change: 0 additions & 1 deletion src/analytics/application/auth-event.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class AuthCreatedEventHandler implements IEventHandler<AuthCreatedEvent>
handle(event: AuthCreatedEvent) {
this.analyticsService.trackEvent(event.userId.value, 'Signed Up', {
email: event.email,
role: event.role,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ICommand } from '@nestjs/cqrs';

export class CreateArticleCommand implements ICommand {
constructor(
public readonly organizationId: string,
public readonly title: string,
public readonly organization: string,
public readonly description: string,
public readonly location: string,
public readonly startAt: string,
public readonly endAt: string,
public readonly tags: string[],
public readonly registrationUrl?: string,
public readonly registrationStartAt?: string,
public readonly registrationEndAt?: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { Identifier } from 'src/shared/core/domain/identifier';
import { Article } from '../../domain/article';
import { CreateArticleRequestDto } from './dto/create-article.request.dto';
import { ARTICLE_COMMAND_REPOSITORY, ArticleCommandRepository } from '../../domain/article.command.repository';
import { CreateArticleResponseDto } from './dto/create-article.response.dto';
import { Tag } from 'src/tag/domain/entity/tag';
import { TAG_REPOSITORY, TagRepository } from 'src/tag/domain/repository/tag.repository';
import { CreateArticleCommand } from './create-article.command';

@Injectable()
export class CreateArticleUseCase {
Expand All @@ -16,11 +16,11 @@ export class CreateArticleUseCase {
private readonly tagRepo: TagRepository,
) {}

async execute(reqDto: CreateArticleRequestDto): Promise<CreateArticleResponseDto> {
async execute(command: CreateArticleCommand): Promise<CreateArticleResponseDto> {
const tags: Tag[] = [];
const articleId = Identifier.create();

for (const tag of reqDto.tags) {
for (const tag of command.tags) {
const existingTag = await this.tagRepo.findByName(tag);

if (!existingTag) {
Expand All @@ -41,15 +41,16 @@ export class CreateArticleUseCase {
// Article 도메인 엔티티 생성
const article = Article.create({
id: articleId,
title: reqDto.title,
organization: reqDto.organization,
description: reqDto.description,
location: reqDto.location,
startAt: new Date(reqDto.startAt),
endAt: new Date(reqDto.endAt),
registrationUrl: reqDto.registrationUrl,
registrationStartAt: reqDto.registrationStartAt ? new Date(reqDto.registrationStartAt) : undefined,
registrationEndAt: reqDto.registrationEndAt ? new Date(reqDto.registrationEndAt) : undefined,
title: command.title,
organizationId: Identifier.from(command.organizationId),
organization: command.organization,
description: command.description,
location: command.location,
startAt: new Date(command.startAt),
endAt: new Date(command.endAt),
registrationUrl: command.registrationUrl,
registrationStartAt: command.registrationStartAt ? new Date(command.registrationStartAt) : undefined,
registrationEndAt: command.registrationEndAt ? new Date(command.registrationEndAt) : undefined,
scrapCount: 0,
viewCount: 0,
mediaIds: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';

export class DeleteArticleCommand implements ICommand {
constructor(
public readonly id: string,
public readonly organizationId: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { ARTICLE_COMMAND_REPOSITORY, ArticleCommandRepository } from '../../domain/article.command.repository';
import { DeleteArticleCommand } from './delete-article.comand';

@Injectable()
export class DeleteArticleUseCase {
Expand All @@ -8,7 +9,7 @@ export class DeleteArticleUseCase {
private readonly articleRepo: ArticleCommandRepository,
) {}

async execute(id: string): Promise<void> {
await this.articleRepo.deleteById(id);
async execute(command: DeleteArticleCommand): Promise<void> {
await this.articleRepo.deleteById(command.id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ICommand } from '@nestjs/cqrs';

export class UpdateArticleCommand implements ICommand {
constructor(
public readonly id: string,
public readonly organizationId: string,
public readonly title?: string,
public readonly organization?: string,
public readonly description?: string,
public readonly location?: string,
public readonly startAt?: string,
public readonly endAt?: string,
public readonly tags?: string[],
public readonly registrationUrl?: string,
public readonly registrationStartAt?: string,
public readonly registrationEndAt?: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Identifier } from 'src/shared/core/domain/identifier';
import { ARTICLE_COMMAND_REPOSITORY, ArticleCommandRepository } from '../../domain/article.command.repository';
import { UpdateArticleRequestDto } from './dto/update-article.request.dto';
import { Tag } from 'src/tag/domain/entity/tag';
import { TAG_REPOSITORY, TagRepository } from 'src/tag/domain/repository/tag.repository';
import { MEDIA_COMMAND_REPOSITORY, MediaCommandRepository } from 'src/media/command/domain/media.command.repository';
import { UpdateArticleCommand } from './update-article.command';

@Injectable()
export class UpdateArticleUseCase {
Expand All @@ -17,15 +17,15 @@ export class UpdateArticleUseCase {
private readonly mediaCommandRepo: MediaCommandRepository,
) {}

async execute(articleId: string, reqDto: UpdateArticleRequestDto): Promise<void> {
async execute(command: UpdateArticleCommand): Promise<void> {
// 기존 Article 조회
const article = await this.articleCommandRepo.findById(articleId);
const article = await this.articleCommandRepo.findById(command.id);

// 태그 처리
if (reqDto.tags !== undefined) {
if (command.tags !== undefined) {
const tags: Tag[] = [];

for (const tagName of reqDto.tags) {
for (const tagName of command.tags) {
const existingTag = await this.tagRepo.findByName(tagName);

if (!existingTag) {
Expand All @@ -49,15 +49,15 @@ export class UpdateArticleUseCase {

// Article 업데이트
article.update({
title: reqDto.title,
organization: reqDto.organization,
description: reqDto.description,
location: reqDto.location,
startAt: reqDto.startAt ? new Date(reqDto.startAt) : undefined,
endAt: reqDto.endAt ? new Date(reqDto.endAt) : undefined,
registrationUrl: reqDto.registrationUrl,
registrationStartAt: reqDto.registrationStartAt ? new Date(reqDto.registrationStartAt) : undefined,
registrationEndAt: reqDto.registrationEndAt ? new Date(reqDto.registrationEndAt) : undefined,
title: command.title,
organization: command.organization,
description: command.description,
location: command.location,
startAt: command.startAt ? new Date(command.startAt) : undefined,
endAt: command.endAt ? new Date(command.endAt) : undefined,
registrationUrl: command.registrationUrl,
registrationStartAt: command.registrationStartAt ? new Date(command.registrationStartAt) : undefined,
registrationEndAt: command.registrationEndAt ? new Date(command.registrationEndAt) : undefined,
});

await this.articleCommandRepo.update(article);
Expand Down
7 changes: 5 additions & 2 deletions src/article/command/article.command.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { ArticleEntity } from './infrastructure/article.entity';
import { ArticleCommandController } from './presentation/article.command.controller';
import {
ArticleCommandController,
OrganizationArticleCommandController,
} from './presentation/article.command.controller';
import { CreateArticleUseCase } from './application/create-article/create-article.use-case';
import { UpdateArticleUseCase } from './application/update-article/update-article.use-case';
import { DeleteArticleUseCase } from './application/delete-article/delete-article.use-case';
Expand Down Expand Up @@ -45,6 +48,6 @@ const listeners = [IncreaseScrapCountListener, DecreaseScrapCountListener];
useClass: MediaCommandRepositoryImpl,
},
],
controllers: [ArticleCommandController],
controllers: [ArticleCommandController, OrganizationArticleCommandController],
})
export class ArticleCommandModule {}
9 changes: 7 additions & 2 deletions src/article/command/domain/article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ export interface ArticleProps extends BaseEntityProps {
organization: string;
location: string;
description: string;
registrationUrl: string;
startAt: Date;
endAt: Date;
registrationUrl?: string;
registrationStartAt?: Date;
registrationEndAt?: Date;
scrapCount: number;
viewCount: number;
mediaIds: Identifier[];
tags: Tag[];
organizationId: Identifier;
}

export class Article extends BaseDomainEntity<ArticleProps> {
Expand Down Expand Up @@ -101,7 +102,7 @@ export class Article extends BaseDomainEntity<ArticleProps> {
return this.props.description;
}

get registrationUrl(): string {
get registrationUrl(): string | undefined {
return this.props.registrationUrl;
}

Expand Down Expand Up @@ -137,6 +138,10 @@ export class Article extends BaseDomainEntity<ArticleProps> {
return this.props.tags;
}

get organizationId(): Identifier {
return this.props.organizationId;
}

public update(props: {
title?: string;
organization?: string;
Expand Down
7 changes: 5 additions & 2 deletions src/article/command/infrastructure/article.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export class ArticleEntity extends BaseEntity {
@Property({ type: 'varchar' })
title: string;

@Property({ type: 'varchar' })
organizationId: string;

@Property({ type: 'varchar' })
organization: string;

Expand All @@ -17,8 +20,8 @@ export class ArticleEntity extends BaseEntity {
@Property({ type: 'varchar', length: 2047 })
description: string;

@Property({ type: 'varchar' })
registrationUrl: string;
@Property({ type: 'varchar', nullable: true })
registrationUrl?: string;

@Property({ type: 'datetime' })
startAt: Date;
Expand Down
2 changes: 2 additions & 0 deletions src/article/command/infrastructure/article.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class ArticleMapper {
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
title: entity.title,
organizationId: Identifier.from(entity.organizationId),
organization: entity.organization,
location: entity.location,
description: entity.description,
Expand All @@ -33,6 +34,7 @@ export class ArticleMapper {
entity.createdAt = domain.createdAt;
entity.updatedAt = domain.updatedAt;
entity.title = domain.title;
entity.organizationId = domain.organizationId.value;
entity.organization = domain.organization;
entity.location = domain.location;
entity.description = domain.description;
Expand Down
58 changes: 52 additions & 6 deletions src/article/command/presentation/article.command.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Body, Controller, Delete, Param, Patch, Post } from '@nestjs/common';
import { Body, Controller, Delete, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { CreateArticleUseCase } from '../application/create-article/create-article.use-case';
import { CreateArticleRequestDto } from '../application/create-article/dto/create-article.request.dto';
import { CreateArticleRequestDto } from './dto/create-article.request.dto';
import { CreateArticleResponseDto } from '../application/create-article/dto/create-article.response.dto';
import { DeleteArticleUseCase } from '../application/delete-article/delete-article.use-case';
import { UpdateArticleUseCase } from '../application/update-article/update-article.use-case';
import { UpdateArticleRequestDto } from '../application/update-article/dto/update-article.request.dto';
import { UpdateArticleRequestDto } from './dto/update-article.request.dto';
import { ArticleCommandDocs } from './article.command.docs';
import { AuthGuard } from '@nestjs/passport';
import { Organization, OrganizationPayload } from 'src/shared/core/presentation/organization.decorator';
import { RolesGuard } from 'src/auth/core/infrastructure/guard/role.guard';
import { Roles } from 'src/shared/core/presentation/role.decorator';
import { Role } from 'src/auth/core/domain/value-object/role';

@ApiTags('article')
@Controller('article')
Expand All @@ -20,18 +25,59 @@ export class ArticleCommandController {
@Post()
@ArticleCommandDocs('create')
async createArticle(@Body() reqDto: CreateArticleRequestDto): Promise<CreateArticleResponseDto> {
return await this.createArticleUseCase.execute(reqDto);
return await this.createArticleUseCase.execute({ ...reqDto, organizationId: '1' });
}

@Patch(':id')
@ArticleCommandDocs('update')
async updateArticle(@Param('id') id: string, @Body() reqDto: UpdateArticleRequestDto): Promise<void> {
return await this.updateArticleUseCase.execute(id, reqDto);
return await this.updateArticleUseCase.execute({ ...reqDto, id, organizationId: '1' });
}

@Delete(':id')
@ArticleCommandDocs('delete')
async delete(@Param('id') id: string): Promise<void> {
return await this.deleteArticleUseCase.execute(id);
return await this.deleteArticleUseCase.execute({ id, organizationId: '1' });
}
}

@ApiTags('organization-article')
@Controller('organization/article')
export class OrganizationArticleCommandController {
constructor(
private readonly createArticleUseCase: CreateArticleUseCase,
private readonly updateArticleUseCase: UpdateArticleUseCase,
private readonly deleteArticleUseCase: DeleteArticleUseCase,
) {}

@Post()
@UseGuards(AuthGuard('jwt-access'), RolesGuard)
@Roles(Role.ORGANIZATION)
@ArticleCommandDocs('create')
async createArticle(
@Organization() organization: OrganizationPayload,
@Body() reqDto: CreateArticleRequestDto,
): Promise<CreateArticleResponseDto> {
return await this.createArticleUseCase.execute({ ...reqDto, organizationId: organization.organizationId });
}

@Patch(':id')
@UseGuards(AuthGuard('jwt-access'), RolesGuard)
@Roles(Role.ORGANIZATION)
@ArticleCommandDocs('update')
async updateArticle(
@Organization() organization: OrganizationPayload,
@Param('id') id: string,
@Body() reqDto: UpdateArticleRequestDto,
): Promise<void> {
return await this.updateArticleUseCase.execute({ ...reqDto, id, organizationId: organization.organizationId });
}

@Delete(':id')
@UseGuards(AuthGuard('jwt-access'), RolesGuard)
@Roles(Role.ORGANIZATION)
@ArticleCommandDocs('delete')
async delete(@Organization() organization: OrganizationPayload, @Param('id') id: string): Promise<void> {
return await this.deleteArticleUseCase.execute({ id, organizationId: organization.organizationId });
}
}
2 changes: 1 addition & 1 deletion src/article/command/presentation/article.command.docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ApiOperation,
} from '@nestjs/swagger';
import { createDocs } from 'src/shared/core/presentation/base.docs';
import { UpdateArticleRequestDto } from '../application/update-article/dto/update-article.request.dto';
import { UpdateArticleRequestDto } from './dto/update-article.request.dto';

export type ArticleCommandEndpoint = 'create' | 'update' | 'delete';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class CreateArticleRequestDto {

@IsString()
@IsOptional()
registrationUrl: string;
registrationUrl?: string;

@IsString()
@IsOptional()
Expand Down
Loading
Loading