Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8c82123
feat: admin aggregate 구현
liveforpresent Dec 18, 2025
78af597
feat: admin view/entity 구현
liveforpresent Dec 18, 2025
0c4a7ad
feat: admin 정보 조회 로직 및 api 구현
liveforpresent Dec 18, 2025
fd252f6
fix: bcrypt를 이용한 비밀번호 생성 로직 이동 (auth-organization -> auth-core)
liveforpresent Dec 18, 2025
9578a94
fix: 기관 회원가입 로직 간단화
liveforpresent Dec 18, 2025
42e8fe9
feat: admin 생성 로직 구현
liveforpresent Dec 19, 2025
4d7cc28
feat: 관리자 회원가입 로직 구현
liveforpresent Dec 19, 2025
127c8e1
chore: admin 테이블명 변경
liveforpresent Dec 19, 2025
7c3d7b3
feat: 관리자 로그인 로직 구현
liveforpresent Dec 19, 2025
bc3f043
feat: AuthAdmin aggregate 구현
liveforpresent Dec 19, 2025
c8a45d0
feat: AuthAdmin entity 및 mapper 구현
liveforpresent Dec 19, 2025
7c1c471
feat: AuthAdminRepository 구현
liveforpresent Dec 19, 2025
e242c4d
feat: AuthAdmin api 구현
liveforpresent Dec 19, 2025
5cb0ddd
feat: AuthAdmin 예외 코드 정리
liveforpresent Dec 19, 2025
85a291f
fix: admin 회원가입 시 비밀번호 암호화 적용
liveforpresent Dec 21, 2025
8376062
feat: 기관 목록 조회 로직 및 api(관리자 전용) 구현
liveforpresent Dec 21, 2025
9bea656
feat: 사용자 목록 조회 로직 및 api(관리자 전용) 구현
liveforpresent Dec 21, 2025
e913415
fix: 기관 목록 조회 시 cursor 기반 조회로 수정
liveforpresent Dec 21, 2025
748872d
fix: 관리자가 Article 조회 시, location, description, registrationUrl 추가
liveforpresent Dec 22, 2025
cd0091b
feat: 관리자 게시물 조회 repository 구현
liveforpresent Dec 22, 2025
6003b50
feat: 관리자 게시물 목록 조회 로직 구현
liveforpresent Dec 22, 2025
da037bc
feat: 관리자 게시물 상세 조회 로직 구현
liveforpresent Dec 22, 2025
42af391
feat: 관리자 게시물 api 구현
liveforpresent Dec 22, 2025
3e89129
feat: admin decorator 구현
liveforpresent Dec 22, 2025
5aa1634
feat: 기관 회원 탈퇴 구현
liveforpresent Jan 5, 2026
2378c32
fix: copilot PR 수정사항 반영
liveforpresent Jan 5, 2026
7bc1a5d
feat: 기관 비밀번호 변경 로직 및 api 구현
liveforpresent Jan 7, 2026
de65710
feat: 도커 메모리/용량 다이어트
liveforpresent Jan 8, 2026
2a9b0a3
chore: 쓸데 없는 부분 삭제 및 패키지 업데이트
liveforpresent Jan 8, 2026
0d07d44
Merge pull request #51 from DevKor-github/feature/docker
liveforpresent Jan 8, 2026
f1cf6fe
feat: admin aggregate 구현
liveforpresent Dec 18, 2025
c785e09
feat: admin view/entity 구현
liveforpresent Dec 18, 2025
de68573
feat: admin 정보 조회 로직 및 api 구현
liveforpresent Dec 18, 2025
121f317
fix: bcrypt를 이용한 비밀번호 생성 로직 이동 (auth-organization -> auth-core)
liveforpresent Dec 18, 2025
327a64a
fix: 기관 회원가입 로직 간단화
liveforpresent Dec 18, 2025
be99ae6
feat: admin 생성 로직 구현
liveforpresent Dec 19, 2025
7f8d83e
feat: 관리자 회원가입 로직 구현
liveforpresent Dec 19, 2025
f71604f
chore: admin 테이블명 변경
liveforpresent Dec 19, 2025
2cf53e7
feat: 관리자 로그인 로직 구현
liveforpresent Dec 19, 2025
e341b96
feat: AuthAdmin aggregate 구현
liveforpresent Dec 19, 2025
d2ee5e5
feat: AuthAdmin entity 및 mapper 구현
liveforpresent Dec 19, 2025
48d82db
feat: AuthAdminRepository 구현
liveforpresent Dec 19, 2025
e28b4c7
feat: AuthAdmin api 구현
liveforpresent Dec 19, 2025
b452000
feat: AuthAdmin 예외 코드 정리
liveforpresent Dec 19, 2025
55c145b
fix: admin 회원가입 시 비밀번호 암호화 적용
liveforpresent Dec 21, 2025
48d1897
feat: 기관 목록 조회 로직 및 api(관리자 전용) 구현
liveforpresent Dec 21, 2025
19b81e3
feat: 사용자 목록 조회 로직 및 api(관리자 전용) 구현
liveforpresent Dec 21, 2025
27974ef
fix: 기관 목록 조회 시 cursor 기반 조회로 수정
liveforpresent Dec 21, 2025
9c8a29d
fix: 관리자가 Article 조회 시, location, description, registrationUrl 추가
liveforpresent Dec 22, 2025
9b1cf37
feat: 관리자 게시물 조회 repository 구현
liveforpresent Dec 22, 2025
05d3ce5
feat: 관리자 게시물 목록 조회 로직 구현
liveforpresent Dec 22, 2025
b0f5637
feat: 관리자 게시물 상세 조회 로직 구현
liveforpresent Dec 22, 2025
897edc7
feat: 관리자 게시물 api 구현
liveforpresent Dec 22, 2025
60ce057
feat: admin decorator 구현
liveforpresent Dec 22, 2025
d5c2eb0
feat: 기관 회원 탈퇴 구현
liveforpresent Jan 5, 2026
76917e8
fix: copilot PR 수정사항 반영
liveforpresent Jan 5, 2026
041310d
feat: 기관 비밀번호 변경 로직 및 api 구현
liveforpresent Jan 7, 2026
c794beb
chore: 최신 사항 반영
liveforpresent Jan 8, 2026
6995e00
Merge branch 'feature/admin' of https://github.com/DevKor-github/EZPZ…
liveforpresent Jan 8, 2026
1232946
feat: Base jwt strategy 구현
liveforpresent Jan 12, 2026
50f53e7
feat: admin jwt strategy 구현 및 적용
liveforpresent Jan 12, 2026
fd96d9d
feat: organization jwt strategy 구현 및 적용
liveforpresent Jan 12, 2026
64a77f7
feat: user jwt strategy 구현 및 적용
liveforpresent Jan 12, 2026
4ae9e85
chore: 쿠키 만료 시간 jwt와 동기화
liveforpresent Jan 12, 2026
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
20 changes: 13 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
FROM node:20

# 1. Build Stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build

RUN yarn install
# 2. Run Stage
FROM node:20-alpine
WORKDIR /app

COPY . .
COPY --from=builder /app/package.json /app/yarn.lock ./
COPY --from=builder /app/dist ./dist

RUN yarn build
RUN yarn install --production --frozen-lockfile

EXPOSE 3000

CMD ["yarn", "start:dev"]
CMD ["node", "dist/src/main.js"]
14 changes: 0 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,3 @@ services:
- '3000:3000'
env_file:
- .env

mysql:
image: mysql:8.0
restart: always
container_name: univent-db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
ports:
- '${DB_HOST_PORT}:${DB_CONTAINER_PORT}'
volumes:
- db-data:/var/lib/mysql

volumes:
db-data:
25 changes: 25 additions & 0 deletions iam/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { ADMIN_REPOSITORY } from './domain/admin.repository';
import { AdminRepositoryImpl } from './infrastructure/admin.repository.impl';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { AdminEntity } from './infrastructure/admin.entity';
import { SharedModule } from 'src/shared/shared.module';
import { AdminController } from './presentation/admin.controller';
import { GetAdminUseCase } from './application/get/get-admin.use-case';
import { CreateAdminUseCase } from './application/create/create.use-case';

const usecases = [GetAdminUseCase, CreateAdminUseCase];

@Module({
imports: [MikroOrmModule.forFeature([AdminEntity]), SharedModule],
providers: [
...usecases,
{
provide: ADMIN_REPOSITORY,
useClass: AdminRepositoryImpl,
},
],
exports: [...usecases],
controllers: [AdminController],
})
export class AdminModule {}
3 changes: 3 additions & 0 deletions iam/admin/application/create/create.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class CreateAdminCommand {
constructor(public readonly name: string) {}
}
3 changes: 3 additions & 0 deletions iam/admin/application/create/create.result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class CreateAdminResult {
adminId: string;
}
29 changes: 29 additions & 0 deletions iam/admin/application/create/create.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ADMIN_REPOSITORY, AdminRepository } from 'iam/admin/domain/admin.repository';
import { CreateAdminCommand } from './create.command';
import { CreateAdminResult } from './create.result';
import { Inject, Injectable } from '@nestjs/common';
import { Admin } from 'iam/admin/domain/admin';
import { Identifier } from 'src/shared/core/domain/identifier';

@Injectable()
export class CreateAdminUseCase {
constructor(
@Inject(ADMIN_REPOSITORY)
private readonly adminRepository: AdminRepository,
) {}

async execute(command: CreateAdminCommand): Promise<CreateAdminResult> {
const { name } = command;

const admin = Admin.create({
id: Identifier.create(),
name,
createdAt: new Date(),
updatedAt: new Date(),
});

await this.adminRepository.save(admin);

return { adminId: admin.id.value };
}
}
7 changes: 7 additions & 0 deletions iam/admin/application/get/get-admin.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class GetAdminQuery {
@IsString()
@IsNotEmpty()
adminId: string;
}
18 changes: 18 additions & 0 deletions iam/admin/application/get/get-admin.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { ADMIN_REPOSITORY, AdminRepository } from 'iam/admin/domain/admin.repository';
import { GetAdminQuery } from './get-admin.query';
import { AdminView } from 'iam/admin/domain/admin.view';

@Injectable()
export class GetAdminUseCase {
constructor(
@Inject(ADMIN_REPOSITORY)
private readonly adminRepository: AdminRepository,
) {}

async execute(query: GetAdminQuery): Promise<AdminView> {
const admin = await this.adminRepository.loadById(query.adminId);

return { name: admin.name };
}
}
8 changes: 8 additions & 0 deletions iam/admin/domain/admin.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Admin } from './admin';

export interface AdminRepository {
save(admin: Admin): Promise<void>;
loadById(id: string): Promise<Admin>;
}

export const ADMIN_REPOSITORY = Symbol('ADMIN_REPOSITORY');
35 changes: 35 additions & 0 deletions iam/admin/domain/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AggregateRoot } from 'src/shared/core/domain/base.aggregate';
import { BaseEntityProps } from 'src/shared/core/domain/base.entity';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';

export interface AdminProps extends BaseEntityProps {
name: string;
}

export class Admin extends AggregateRoot<AdminProps> {
protected constructor(props: AdminProps) {
super(props);
}

public static create(props: AdminProps): Admin {
const admin = new Admin(props);
admin.validate();

return admin;
}

public static of(props: AdminProps): Admin {
return new Admin(props);
}

public validate(): void {
if (this.props.name.length < 1 || this.props.name.length > 20) {
throw new CustomException(CustomExceptionCode.ADMIN_INVALID_NAME_LENGTH);
}
}

get name(): string {
return this.props.name;
}
}
9 changes: 9 additions & 0 deletions iam/admin/domain/admin.view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';

export class AdminView {
@ApiProperty({
example: '이름',
description: '관리자 이름',
})
name: string;
}
8 changes: 8 additions & 0 deletions iam/admin/infrastructure/admin.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Entity, Property } from '@mikro-orm/core';
import { BaseEntity } from 'src/shared/core/infrastructure/orm-entity/base.entity';

@Entity({ tableName: 'admin' })
export class AdminEntity extends BaseEntity {
@Property({ type: 'varchar' })
name: string;
}
24 changes: 24 additions & 0 deletions iam/admin/infrastructure/admin.mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Identifier } from 'src/shared/core/domain/identifier';
import { Admin } from '../domain/admin';
import { AdminEntity } from './admin.entity';

export class AdminMapper {
static toDomain(entity: AdminEntity): Admin {
return Admin.create({
id: Identifier.from(entity.id),
name: entity.name,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
});
}

static toEntity(domain: Admin): AdminEntity {
const entity = new AdminEntity();
entity.id = domain.id.value;
entity.name = domain.name;
entity.createdAt = domain.createdAt;
entity.updatedAt = domain.updatedAt;

return entity;
}
}
28 changes: 28 additions & 0 deletions iam/admin/infrastructure/admin.repository.impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { EntityManager, EntityRepository } from '@mikro-orm/mysql';
import { Admin } from '../domain/admin';
import { AdminRepository } from '../domain/admin.repository';
import { AdminMapper } from './admin.mapper';
import { AdminEntity } from './admin.entity';
import { InjectRepository } from '@mikro-orm/nestjs';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';

export class AdminRepositoryImpl implements AdminRepository {
constructor(
@InjectRepository(AdminEntity)
private readonly ormRepository: EntityRepository<AdminEntity>,
private readonly em: EntityManager,
) {}

async save(admin: Admin): Promise<void> {
const entity = AdminMapper.toEntity(admin);
await this.em.persistAndFlush(entity);
}

async loadById(id: string): Promise<Admin> {
const entity = await this.ormRepository.findOne({ id });
if (!entity) throw new CustomException(CustomExceptionCode.ADMIN_NOT_FOUND);

return AdminMapper.toDomain(entity);
}
}
22 changes: 22 additions & 0 deletions iam/admin/presentation/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { GetAdminUseCase } from '../application/get/get-admin.use-case';
import { RolesGuard } from 'iam/auth/auth-core/infrastructure/guard/role.guard';
import { AuthGuard } from '@nestjs/passport';
import { Roles } from 'src/shared/core/presentation/role.decorator';
import { Role } from 'iam/auth/auth-core/domain/value-object/role';
import { Admin, AdminPayload } from 'src/shared/core/presentation/admin.decorator';
import { AdminView } from '../domain/admin.view';

@ApiTags('admin')
@Controller('admin')
export class AdminController {
constructor(private readonly getAdminUseCase: GetAdminUseCase) {}

@Get()
@UseGuards(AuthGuard('jwt-admin-access'), RolesGuard)
@Roles(Role.ADMIN)
async getAdminInfo(@Admin() admin: AdminPayload): Promise<AdminView> {
return await this.getAdminUseCase.execute({ adminId: admin.adminId });
}
}
6 changes: 6 additions & 0 deletions iam/auth/auth-admin/application/login/login.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class AdminLoginCommand {
constructor(
public readonly accountId: string,
public readonly password: string,
) {}
}
3 changes: 3 additions & 0 deletions iam/auth/auth-admin/application/login/login.result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class AdminLoginResult {
accessToken: string;
}
47 changes: 47 additions & 0 deletions iam/auth/auth-admin/application/login/login.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Inject, Injectable } from '@nestjs/common';
import { AUTH_ADMIN_REPOSITORY, AuthAdminRepository } from '../../domain/auth-admin.repository';
import { AdminLoginCommand } from './login.command';
import { PASSWORD_HASHER, PasswordHasher } from 'iam/auth/auth-core/domain/password-hasher';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';
import { TokenType } from 'iam/auth/auth-core/infrastructure/jwt/jwt.factory';
import { AuthAdmin } from '../../domain/auth-admin';
import { Role } from 'iam/auth/auth-core/domain/value-object/role';
import { JwtProvider } from 'iam/auth/auth-core/infrastructure/jwt/jwt.provider';
import { AdminLoginResult } from './login.result';

@Injectable()
export class AdminLoginUseCase {
constructor(
@Inject(AUTH_ADMIN_REPOSITORY)
private readonly authAdminRepository: AuthAdminRepository,
@Inject(PASSWORD_HASHER)
private readonly passwordHasher: PasswordHasher,
private readonly jwtProvider: JwtProvider,
) {}

async execute(command: AdminLoginCommand): Promise<AdminLoginResult> {
const { accountId, password } = command;

const authAdmin = await this.validateAccount(accountId.trim(), password.trim());
const { accessToken } = await this.generateToken(authAdmin.adminId.value);

return { accessToken };
}

private async validateAccount(accountId: string, password: string): Promise<AuthAdmin> {
const authAdmin = await this.authAdminRepository.loadByAccountId(accountId);
if (!authAdmin) throw new CustomException(CustomExceptionCode.AUTH_ADMIN_NOT_FOUND);

const isPasswordValid = await this.passwordHasher.compare(password, authAdmin.passwordHash.value);
if (!isPasswordValid) throw new CustomException(CustomExceptionCode.AUTH_INVALID_PASSWORD);

return authAdmin;
}

private async generateToken(adminId: string): Promise<{ accessToken: string }> {
const { token: accessToken } = await this.jwtProvider.generateToken(TokenType.ACCESS, adminId, [Role.ADMIN]);

return { accessToken };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class RegisterAdminCommand {
constructor(
public readonly accountId: string,
public readonly password: string,
public readonly name: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class RegisterAdminResult {
accessToken: string;
}
Loading