This commit is contained in:
djeon
2025-10-23 14:55:33 +09:00
parent 41d57e7399
commit 98ede67f62
109 changed files with 8633 additions and 0 deletions
@@ -0,0 +1,77 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 대시보드 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Dashboard {
/**
* 다가오는 회의 목록
*/
private List<Meeting> upcomingMeetings;
/**
* 최근 회의록 목록
*/
private List<Minutes> recentMinutes;
/**
* 할당된 Todo 목록
*/
private List<Todo> assignedTodos;
/**
* 통계 정보
*/
private Statistics statistics;
/**
* 통계 정보 내부 클래스
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Statistics {
/**
* 전체 회의 수
*/
private Integer totalMeetings;
/**
* 진행 중인 회의 수
*/
private Integer inProgressMeetings;
/**
* 완료된 회의 수
*/
private Integer completedMeetings;
/**
* 전체 Todo 수
*/
private Integer totalTodos;
/**
* 완료된 Todo 수
*/
private Integer completedTodos;
/**
* 지연된 Todo 수
*/
private Integer overdueTodos;
}
}
@@ -0,0 +1,99 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 회의 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Meeting {
/**
* 회의 ID
*/
private String meetingId;
/**
* 회의 제목
*/
private String title;
/**
* 회의 설명
*/
private String description;
/**
* 회의 일시
*/
private LocalDateTime scheduledAt;
/**
* 회의 시작 일시
*/
private LocalDateTime startedAt;
/**
* 회의 종료 일시
*/
private LocalDateTime endedAt;
/**
* 회의 상태 (SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED)
*/
private String status;
/**
* 주최자 ID
*/
private String organizerId;
/**
* 참석자 ID 목록
*/
private List<String> participants;
/**
* 템플릿 ID
*/
private String templateId;
/**
* 회의 시작
*/
public void start() {
this.status = "IN_PROGRESS";
this.startedAt = LocalDateTime.now();
}
/**
* 회의 종료
*/
public void end() {
this.status = "COMPLETED";
this.endedAt = LocalDateTime.now();
}
/**
* 회의 취소
*/
public void cancel() {
this.status = "CANCELLED";
}
/**
* 회의 진행 중 여부 확인
*/
public boolean isInProgress() {
return "IN_PROGRESS".equals(this.status);
}
}
@@ -0,0 +1,87 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 회의록 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Minutes {
/**
* 회의록 ID
*/
private String minutesId;
/**
* 회의 ID
*/
private String meetingId;
/**
* 회의록 제목
*/
private String title;
/**
* 회의록 섹션 목록
*/
private List<MinutesSection> sections;
/**
* 회의록 상태 (DRAFT, FINALIZED)
*/
private String status;
/**
* 버전
*/
private Integer version;
/**
* 작성자 ID
*/
private String createdBy;
/**
* 확정자 ID
*/
private String finalizedBy;
/**
* 확정 일시
*/
private LocalDateTime finalizedAt;
/**
* 회의록 확정
*/
public void finalize(String userId) {
this.status = "FINALIZED";
this.finalizedBy = userId;
this.finalizedAt = LocalDateTime.now();
}
/**
* 회의록 수정
*/
public void update() {
this.version++;
}
/**
* 회의록 확정 여부 확인
*/
public boolean isFinalized() {
return "FINALIZED".equals(this.status);
}
}
@@ -0,0 +1,98 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 회의록 섹션 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MinutesSection {
/**
* 섹션 ID
*/
private String sectionId;
/**
* 회의록 ID
*/
private String minutesId;
/**
* 섹션 유형 (AGENDA, DISCUSSION, DECISION, ACTION_ITEM)
*/
private String type;
/**
* 섹션 제목
*/
private String title;
/**
* 섹션 내용
*/
private String content;
/**
* 섹션 순서
*/
private Integer order;
/**
* 검증 완료 여부
*/
private Boolean verified;
/**
* 잠금 여부
*/
private Boolean locked;
/**
* 잠금 사용자 ID
*/
private String lockedBy;
/**
* 섹션 잠금
*/
public void lock(String userId) {
this.locked = true;
this.lockedBy = userId;
}
/**
* 섹션 잠금 해제
*/
public void unlock() {
this.locked = false;
this.lockedBy = null;
}
/**
* 섹션 검증 완료
*/
public void verify() {
this.verified = true;
}
/**
* 섹션 잠금 여부 확인
*/
public boolean isLocked() {
return Boolean.TRUE.equals(this.locked);
}
/**
* 섹션 검증 완료 여부 확인
*/
public boolean isVerified() {
return Boolean.TRUE.equals(this.verified);
}
}
@@ -0,0 +1,82 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 회의록 템플릿 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Template {
/**
* 템플릿 ID
*/
private String templateId;
/**
* 템플릿 이름
*/
private String name;
/**
* 템플릿 설명
*/
private String description;
/**
* 템플릿 카테고리 (GENERAL, TECHNICAL, MANAGEMENT, SALES)
*/
private String category;
/**
* 템플릿 섹션 목록
*/
private List<TemplateSection> sections;
/**
* 공개 여부
*/
private Boolean isPublic;
/**
* 생성자 ID
*/
private String createdBy;
/**
* 템플릿 섹션 내부 클래스
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TemplateSection {
/**
* 섹션 유형
*/
private String type;
/**
* 섹션 제목
*/
private String title;
/**
* 섹션 순서
*/
private Integer order;
/**
* 기본 내용
*/
private String defaultContent;
}
}
@@ -0,0 +1,100 @@
package com.unicorn.hgzero.meeting.biz.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* Todo 도메인 모델
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Todo {
/**
* Todo ID
*/
private String todoId;
/**
* 회의록 ID
*/
private String minutesId;
/**
* 회의 ID
*/
private String meetingId;
/**
* Todo 제목
*/
private String title;
/**
* Todo 설명
*/
private String description;
/**
* 담당자 ID
*/
private String assigneeId;
/**
* 마감일
*/
private LocalDate dueDate;
/**
* Todo 상태 (PENDING, IN_PROGRESS, COMPLETED, CANCELLED)
*/
private String status;
/**
* 우선순위 (HIGH, MEDIUM, LOW)
*/
private String priority;
/**
* 완료 일시
*/
private LocalDateTime completedAt;
/**
* Todo 완료
*/
public void complete() {
this.status = "COMPLETED";
this.completedAt = LocalDateTime.now();
}
/**
* Todo 취소
*/
public void cancel() {
this.status = "CANCELLED";
}
/**
* Todo 완료 여부 확인
*/
public boolean isCompleted() {
return "COMPLETED".equals(this.status);
}
/**
* 마감일 지남 여부 확인
*/
public boolean isOverdue() {
return this.dueDate != null &&
LocalDate.now().isAfter(this.dueDate) &&
!isCompleted();
}
}
@@ -0,0 +1,43 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
import com.unicorn.hgzero.meeting.biz.usecase.in.dashboard.GetDashboardUseCase;
import com.unicorn.hgzero.meeting.biz.usecase.out.DashboardReader;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 대시보드 Service
* Dashboard 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DashboardService implements GetDashboardUseCase {
private final DashboardReader dashboardReader;
/**
* 사용자 대시보드 조회
*/
@Override
@Transactional(readOnly = true)
public Dashboard getDashboard(String userId) {
log.debug("Getting dashboard for user: {}", userId);
return dashboardReader.getDashboardByUserId(userId);
}
/**
* 사용자 대시보드 (기간 필터) 조회
*/
@Override
@Transactional(readOnly = true)
public Dashboard getDashboardByPeriod(String userId, String period) {
log.debug("Getting dashboard for user: {} with period: {}", userId, period);
return dashboardReader.getDashboardByUserIdAndPeriod(userId, period);
}
}
@@ -0,0 +1,201 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 회의 Service
* Meeting 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MeetingService implements
CreateMeetingUseCase,
StartMeetingUseCase,
EndMeetingUseCase,
CancelMeetingUseCase,
GetMeetingUseCase {
private final MeetingReader meetingReader;
private final MeetingWriter meetingWriter;
/**
* 회의 생성
*/
@Override
@Transactional
public Meeting createMeeting(CreateMeetingCommand command) {
log.info("Creating meeting: {}", command.title());
// 회의 ID 생성
String meetingId = UUID.randomUUID().toString();
// 회의 도메인 객체 생성
Meeting meeting = Meeting.builder()
.meetingId(meetingId)
.title(command.title())
.description(command.description())
.scheduledAt(command.scheduledAt())
.status("SCHEDULED")
.organizerId(command.organizerId())
.participants(command.participants())
.templateId(command.templateId())
.build();
// 회의 저장
Meeting savedMeeting = meetingWriter.save(meeting);
log.info("Meeting created successfully: {}", savedMeeting.getMeetingId());
return savedMeeting;
}
/**
* 회의 시작
*/
@Override
@Transactional
public Meeting startMeeting(String meetingId) {
log.info("Starting meeting: {}", meetingId);
// 회의 조회
Meeting meeting = meetingReader.findById(meetingId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 회의 상태 검증
if (!"SCHEDULED".equals(meeting.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 회의 시작
meeting.start();
// 저장
Meeting updatedMeeting = meetingWriter.save(meeting);
log.info("Meeting started successfully: {}", meetingId);
return updatedMeeting;
}
/**
* 회의 종료
*/
@Override
@Transactional
public Meeting endMeeting(String meetingId) {
log.info("Ending meeting: {}", meetingId);
// 회의 조회
Meeting meeting = meetingReader.findById(meetingId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 회의 상태 검증
if (!"IN_PROGRESS".equals(meeting.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 회의 종료
meeting.end();
// 저장
Meeting updatedMeeting = meetingWriter.save(meeting);
log.info("Meeting ended successfully: {}", meetingId);
return updatedMeeting;
}
/**
* 회의 취소
*/
@Override
@Transactional
public Meeting cancelMeeting(String meetingId) {
log.info("Canceling meeting: {}", meetingId);
// 회의 조회
Meeting meeting = meetingReader.findById(meetingId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 회의 취소 가능 상태 검증
if ("COMPLETED".equals(meeting.getStatus()) || "CANCELLED".equals(meeting.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 회의 취소
meeting.cancel();
// 저장
Meeting updatedMeeting = meetingWriter.save(meeting);
log.info("Meeting cancelled successfully: {}", meetingId);
return updatedMeeting;
}
/**
* ID로 회의 조회
*/
@Override
@Transactional(readOnly = true)
public Meeting getMeeting(String meetingId) {
log.debug("Getting meeting: {}", meetingId);
return meetingReader.findById(meetingId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 주최자 ID로 회의 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Meeting> getMeetingsByOrganizer(String organizerId) {
log.debug("Getting meetings by organizer: {}", organizerId);
return meetingReader.findByOrganizerId(organizerId);
}
/**
* 상태로 회의 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Meeting> getMeetingsByStatus(String status) {
log.debug("Getting meetings by status: {}", status);
return meetingReader.findByStatus(status);
}
/**
* 일정 시간 범위로 회의 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Meeting> getMeetingsByScheduledTime(LocalDateTime startTime, LocalDateTime endTime) {
log.debug("Getting meetings by scheduled time: {} ~ {}", startTime, endTime);
return meetingReader.findByScheduledTimeBetween(startTime, endTime);
}
/**
* 주최자 ID와 상태로 회의 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Meeting> getMeetingsByOrganizerAndStatus(String organizerId, String status) {
log.debug("Getting meetings by organizer: {} and status: {}", organizerId, status);
return meetingReader.findByOrganizerIdAndStatus(organizerId, status);
}
}
@@ -0,0 +1,234 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
import com.unicorn.hgzero.meeting.biz.usecase.in.section.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 회의록 섹션 Service
* MinutesSection 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MinutesSectionService implements
CreateSectionUseCase,
UpdateSectionUseCase,
DeleteSectionUseCase,
LockSectionUseCase,
VerifySectionUseCase,
GetSectionUseCase {
private final MinutesSectionReader sectionReader;
private final MinutesSectionWriter sectionWriter;
/**
* 섹션 생성
*/
@Override
@Transactional
public MinutesSection createSection(CreateSectionCommand command) {
log.info("Creating section for minutes: {}", command.minutesId());
// 섹션 ID 생성
String sectionId = UUID.randomUUID().toString();
// 섹션 도메인 객체 생성
MinutesSection section = MinutesSection.builder()
.sectionId(sectionId)
.minutesId(command.minutesId())
.type(command.type())
.title(command.title())
.content(command.content())
.order(command.order())
.verified(false)
.locked(false)
.build();
// 섹션 저장
MinutesSection savedSection = sectionWriter.save(section);
log.info("Section created successfully: {}", savedSection.getSectionId());
return savedSection;
}
/**
* 섹션 내용 수정
*/
@Override
@Transactional
public MinutesSection updateSection(UpdateSectionCommand command) {
log.info("Updating section: {}", command.sectionId());
// 섹션 조회
MinutesSection section = sectionReader.findById(command.sectionId())
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 잠금 상태 검증
if (Boolean.TRUE.equals(section.getLocked())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 검증 상태 검증
if (Boolean.TRUE.equals(section.getVerified())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 섹션 수정
section.update(command.title(), command.content());
// 저장
MinutesSection updatedSection = sectionWriter.save(section);
log.info("Section updated successfully: {}", command.sectionId());
return updatedSection;
}
/**
* 섹션 삭제
*/
@Override
@Transactional
public void deleteSection(String sectionId) {
log.info("Deleting section: {}", sectionId);
// 섹션 조회
MinutesSection section = sectionReader.findById(sectionId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 검증 상태 확인 (검증된 섹션은 삭제 불가)
if (Boolean.TRUE.equals(section.getVerified())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 섹션 삭제
sectionWriter.delete(sectionId);
log.info("Section deleted successfully: {}", sectionId);
}
/**
* 섹션 잠금
*/
@Override
@Transactional
public MinutesSection lockSection(String sectionId, String userId) {
log.info("Locking section: {} by user: {}", sectionId, userId);
// 섹션 조회
MinutesSection section = sectionReader.findById(sectionId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 이미 잠금 상태 확인
if (Boolean.TRUE.equals(section.getLocked())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 섹션 잠금
section.lock(userId);
// 저장
MinutesSection lockedSection = sectionWriter.save(section);
log.info("Section locked successfully: {}", sectionId);
return lockedSection;
}
/**
* 섹션 잠금 해제
*/
@Override
@Transactional
public MinutesSection unlockSection(String sectionId) {
log.info("Unlocking section: {}", sectionId);
// 섹션 조회
MinutesSection section = sectionReader.findById(sectionId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 잠금 상태 확인
if (Boolean.FALSE.equals(section.getLocked())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 섹션 잠금 해제
section.unlock();
// 저장
MinutesSection unlockedSection = sectionWriter.save(section);
log.info("Section unlocked successfully: {}", sectionId);
return unlockedSection;
}
/**
* 섹션 검증
*/
@Override
@Transactional
public MinutesSection verifySection(String sectionId) {
log.info("Verifying section: {}", sectionId);
// 섹션 조회
MinutesSection section = sectionReader.findById(sectionId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 이미 검증 상태 확인
if (Boolean.TRUE.equals(section.getVerified())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 섹션 검증
section.verify();
// 저장
MinutesSection verifiedSection = sectionWriter.save(section);
log.info("Section verified successfully: {}", sectionId);
return verifiedSection;
}
/**
* ID로 섹션 조회
*/
@Override
@Transactional(readOnly = true)
public MinutesSection getSection(String sectionId) {
log.debug("Getting section: {}", sectionId);
return sectionReader.findById(sectionId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 회의록 ID로 섹션 목록 조회 (순서대로)
*/
@Override
@Transactional(readOnly = true)
public List<MinutesSection> getSectionsByMinutes(String minutesId) {
log.debug("Getting sections by minutes: {}", minutesId);
return sectionReader.findByMinutesIdOrderByOrder(minutesId);
}
/**
* 회의록 ID와 타입으로 섹션 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<MinutesSection> getSectionsByMinutesAndType(String minutesId, String type) {
log.debug("Getting sections by minutes: {} and type: {}", minutesId, type);
return sectionReader.findByMinutesIdAndType(minutesId, type);
}
}
@@ -0,0 +1,193 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import com.unicorn.hgzero.meeting.biz.usecase.in.minutes.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 회의록 Service
* Minutes 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MinutesService implements
CreateMinutesUseCase,
UpdateMinutesUseCase,
FinalizeMinutesUseCase,
GetMinutesUseCase {
private final MinutesReader minutesReader;
private final MinutesWriter minutesWriter;
/**
* 회의록 생성
*/
@Override
@Transactional
public Minutes createMinutes(CreateMinutesCommand command) {
log.info("Creating minutes for meeting: {}", command.meetingId());
// 회의록 ID 생성
String minutesId = UUID.randomUUID().toString();
// 회의록 도메인 객체 생성
Minutes minutes = Minutes.builder()
.minutesId(minutesId)
.meetingId(command.meetingId())
.title(command.title())
.status("DRAFT")
.version(1)
.createdBy(command.createdBy())
.build();
// 회의록 저장
Minutes savedMinutes = minutesWriter.save(minutes);
log.info("Minutes created successfully: {}", savedMinutes.getMinutesId());
return savedMinutes;
}
/**
* 회의록 제목 수정
*/
@Override
@Transactional
public Minutes updateMinutesTitle(String minutesId, String title) {
log.info("Updating minutes title: {}", minutesId);
// 회의록 조회
Minutes minutes = minutesReader.findById(minutesId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 상태 검증 (확정된 회의록은 수정 불가)
if ("FINALIZED".equals(minutes.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 제목 수정
minutes.update(title, minutes.getSections());
// 저장
Minutes updatedMinutes = minutesWriter.save(minutes);
log.info("Minutes title updated successfully: {}", minutesId);
return updatedMinutes;
}
/**
* 회의록 버전 증가
*/
@Override
@Transactional
public Minutes incrementVersion(String minutesId) {
log.info("Incrementing minutes version: {}", minutesId);
// 회의록 조회
Minutes minutes = minutesReader.findById(minutesId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 버전 증가
minutes.incrementVersion();
// 저장
Minutes updatedMinutes = minutesWriter.save(minutes);
log.info("Minutes version incremented: {} -> {}", minutesId, updatedMinutes.getVersion());
return updatedMinutes;
}
/**
* 회의록 확정
*/
@Override
@Transactional
public Minutes finalizeMinutes(String minutesId, String userId) {
log.info("Finalizing minutes: {}", minutesId);
// 회의록 조회
Minutes minutes = minutesReader.findById(minutesId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 상태 검증
if ("FINALIZED".equals(minutes.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// 회의록 확정
minutes.finalize(userId);
// 저장
Minutes finalizedMinutes = minutesWriter.save(minutes);
log.info("Minutes finalized successfully: {}", minutesId);
return finalizedMinutes;
}
/**
* ID로 회의록 조회
*/
@Override
@Transactional(readOnly = true)
public Minutes getMinutes(String minutesId) {
log.debug("Getting minutes: {}", minutesId);
return minutesReader.findById(minutesId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 회의 ID로 회의록 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Minutes> getMinutesByMeeting(String meetingId) {
log.debug("Getting minutes by meeting: {}", meetingId);
return minutesReader.findByMeetingId(meetingId);
}
/**
* 회의 ID로 최신 회의록 조회
*/
@Override
@Transactional(readOnly = true)
public Minutes getLatestMinutes(String meetingId) {
log.debug("Getting latest minutes for meeting: {}", meetingId);
return minutesReader.findLatestByMeetingId(meetingId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 작성자 ID로 회의록 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Minutes> getMinutesByCreator(String createdBy) {
log.debug("Getting minutes by creator: {}", createdBy);
return minutesReader.findByCreatedBy(createdBy);
}
/**
* 상태로 회의록 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Minutes> getMinutesByStatus(String status) {
log.debug("Getting minutes by status: {}", status);
return minutesReader.findByStatus(status);
}
}
@@ -0,0 +1,116 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Template;
import com.unicorn.hgzero.meeting.biz.usecase.in.template.CreateTemplateUseCase;
import com.unicorn.hgzero.meeting.biz.usecase.in.template.GetTemplateUseCase;
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 템플릿 Service
* Template 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TemplateService implements
CreateTemplateUseCase,
GetTemplateUseCase {
private final TemplateReader templateReader;
private final TemplateWriter templateWriter;
/**
* 템플릿 생성
*/
@Override
@Transactional
public Template createTemplate(CreateTemplateCommand command) {
log.info("Creating template: {}", command.name());
// 템플릿 ID 생성
String templateId = UUID.randomUUID().toString();
// 템플릿 도메인 객체 생성
Template template = Template.builder()
.templateId(templateId)
.name(command.name())
.description(command.description())
.category(command.category())
.sections(command.sections())
.isPublic(command.isPublic() != null ? command.isPublic() : true)
.createdBy(command.createdBy())
.build();
// 템플릿 저장
Template savedTemplate = templateWriter.save(template);
log.info("Template created successfully: {}", savedTemplate.getTemplateId());
return savedTemplate;
}
/**
* ID로 템플릿 조회
*/
@Override
@Transactional(readOnly = true)
public Template getTemplate(String templateId) {
log.debug("Getting template: {}", templateId);
return templateReader.findById(templateId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 카테고리로 템플릿 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Template> getTemplatesByCategory(String category) {
log.debug("Getting templates by category: {}", category);
return templateReader.findByCategory(category);
}
/**
* 공개 템플릿 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Template> getPublicTemplates() {
log.debug("Getting public templates");
return templateReader.findByIsPublic(true);
}
/**
* 작성자 ID로 템플릿 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Template> getTemplatesByCreator(String createdBy) {
log.debug("Getting templates by creator: {}", createdBy);
return templateReader.findByCreatedBy(createdBy);
}
/**
* 이름으로 템플릿 검색
*/
@Override
@Transactional(readOnly = true)
public List<Template> searchTemplatesByName(String name) {
log.debug("Searching templates by name: {}", name);
return templateReader.findByNameContaining(name);
}
}
@@ -0,0 +1,219 @@
package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import com.unicorn.hgzero.meeting.biz.usecase.in.todo.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
/**
* Todo Service
* Todo 관련 모든 UseCase 구현
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TodoService implements
CreateTodoUseCase,
UpdateTodoUseCase,
CompleteTodoUseCase,
CancelTodoUseCase,
GetTodoUseCase {
private final TodoReader todoReader;
private final TodoWriter todoWriter;
/**
* Todo 생성
*/
@Override
@Transactional
public Todo createTodo(CreateTodoCommand command) {
log.info("Creating todo: {}", command.title());
// Todo ID 생성
String todoId = UUID.randomUUID().toString();
// Todo 도메인 객체 생성
Todo todo = Todo.builder()
.todoId(todoId)
.minutesId(command.minutesId())
.meetingId(command.meetingId())
.title(command.title())
.description(command.description())
.assigneeId(command.assigneeId())
.dueDate(command.dueDate())
.status("PENDING")
.priority(command.priority() != null ? command.priority() : "MEDIUM")
.build();
// Todo 저장
Todo savedTodo = todoWriter.save(todo);
log.info("Todo created successfully: {}", savedTodo.getTodoId());
return savedTodo;
}
/**
* Todo 수정
*/
@Override
@Transactional
public Todo updateTodo(UpdateTodoCommand command) {
log.info("Updating todo: {}", command.todoId());
// Todo 조회
Todo todo = todoReader.findById(command.todoId())
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 완료/취소 상태 검증
if ("COMPLETED".equals(todo.getStatus()) || "CANCELLED".equals(todo.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// Todo 수정
todo.update(
command.title(),
command.description(),
command.assigneeId(),
command.dueDate(),
command.priority()
);
// 저장
Todo updatedTodo = todoWriter.save(todo);
log.info("Todo updated successfully: {}", command.todoId());
return updatedTodo;
}
/**
* Todo 완료
*/
@Override
@Transactional
public Todo completeTodo(String todoId) {
log.info("Completing todo: {}", todoId);
// Todo 조회
Todo todo = todoReader.findById(todoId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 상태 검증
if ("COMPLETED".equals(todo.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// Todo 완료
todo.complete();
// 저장
Todo completedTodo = todoWriter.save(todo);
log.info("Todo completed successfully: {}", todoId);
return completedTodo;
}
/**
* Todo 취소
*/
@Override
@Transactional
public Todo cancelTodo(String todoId) {
log.info("Cancelling todo: {}", todoId);
// Todo 조회
Todo todo = todoReader.findById(todoId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
// 상태 검증
if ("COMPLETED".equals(todo.getStatus()) || "CANCELLED".equals(todo.getStatus())) {
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
// Todo 취소
todo.cancel();
// 저장
Todo cancelledTodo = todoWriter.save(todo);
log.info("Todo cancelled successfully: {}", todoId);
return cancelledTodo;
}
/**
* ID로 Todo 조회
*/
@Override
@Transactional(readOnly = true)
public Todo getTodo(String todoId) {
log.debug("Getting todo: {}", todoId);
return todoReader.findById(todoId)
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
}
/**
* 회의 ID로 Todo 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Todo> getTodosByMeeting(String meetingId) {
log.debug("Getting todos by meeting: {}", meetingId);
return todoReader.findByMeetingId(meetingId);
}
/**
* 회의록 ID로 Todo 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Todo> getTodosByMinutes(String minutesId) {
log.debug("Getting todos by minutes: {}", minutesId);
return todoReader.findByMinutesId(minutesId);
}
/**
* 담당자 ID로 Todo 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Todo> getTodosByAssignee(String assigneeId) {
log.debug("Getting todos by assignee: {}", assigneeId);
return todoReader.findByAssigneeId(assigneeId);
}
/**
* 담당자 ID와 상태로 Todo 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Todo> getTodosByAssigneeAndStatus(String assigneeId, String status) {
log.debug("Getting todos by assignee: {} and status: {}", assigneeId, status);
return todoReader.findByAssigneeIdAndStatus(assigneeId, status);
}
/**
* 담당자 ID와 마감일 범위로 Todo 목록 조회
*/
@Override
@Transactional(readOnly = true)
public List<Todo> getTodosByAssigneeAndDueDateRange(String assigneeId, LocalDate startDate, LocalDate endDate) {
log.debug("Getting todos by assignee: {} and due date range: {} ~ {}", assigneeId, startDate, endDate);
return todoReader.findByAssigneeIdAndDueDateBetween(assigneeId, startDate, endDate);
}
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.dashboard;
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
/**
* 대시보드 조회 UseCase
*/
public interface GetDashboardUseCase {
/**
* 사용자 대시보드 조회
*/
Dashboard getDashboard(String userId);
/**
* 사용자 대시보드 (기간 필터) 조회
*/
Dashboard getDashboardByPeriod(String userId, String period);
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
/**
* 회의 취소 UseCase
*/
public interface CancelMeetingUseCase {
/**
* 회의 취소
*/
Meeting cancelMeeting(String meetingId);
}
@@ -0,0 +1,29 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import java.time.LocalDateTime;
import java.util.List;
/**
* 회의 생성 UseCase
*/
public interface CreateMeetingUseCase {
/**
* 회의 생성
*/
Meeting createMeeting(CreateMeetingCommand command);
/**
* 회의 생성 명령
*/
record CreateMeetingCommand(
String title,
String description,
LocalDateTime scheduledAt,
String organizerId,
List<String> participants,
String templateId
) {}
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
/**
* 회의 종료 UseCase
*/
public interface EndMeetingUseCase {
/**
* 회의 종료
*/
Meeting endMeeting(String meetingId);
}
@@ -0,0 +1,37 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import java.time.LocalDateTime;
import java.util.List;
/**
* 회의 조회 UseCase
*/
public interface GetMeetingUseCase {
/**
* ID로 회의 조회
*/
Meeting getMeeting(String meetingId);
/**
* 주최자 ID로 회의 목록 조회
*/
List<Meeting> getMeetingsByOrganizer(String organizerId);
/**
* 상태로 회의 목록 조회
*/
List<Meeting> getMeetingsByStatus(String status);
/**
* 일정 시간 범위로 회의 목록 조회
*/
List<Meeting> getMeetingsByScheduledTime(LocalDateTime startTime, LocalDateTime endTime);
/**
* 주최자 ID와 상태로 회의 목록 조회
*/
List<Meeting> getMeetingsByOrganizerAndStatus(String organizerId, String status);
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
/**
* 회의 시작 UseCase
*/
public interface StartMeetingUseCase {
/**
* 회의 시작
*/
Meeting startMeeting(String meetingId);
}
@@ -0,0 +1,23 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
/**
* 회의록 생성 UseCase
*/
public interface CreateMinutesUseCase {
/**
* 회의록 생성
*/
Minutes createMinutes(CreateMinutesCommand command);
/**
* 회의록 생성 명령
*/
record CreateMinutesCommand(
String meetingId,
String title,
String createdBy
) {}
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
/**
* 회의록 확정 UseCase
*/
public interface FinalizeMinutesUseCase {
/**
* 회의록 확정
*/
Minutes finalizeMinutes(String minutesId, String userId);
}
@@ -0,0 +1,36 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import java.util.List;
/**
* 회의록 조회 UseCase
*/
public interface GetMinutesUseCase {
/**
* ID로 회의록 조회
*/
Minutes getMinutes(String minutesId);
/**
* 회의 ID로 회의록 목록 조회
*/
List<Minutes> getMinutesByMeeting(String meetingId);
/**
* 회의 ID로 최신 회의록 조회
*/
Minutes getLatestMinutes(String meetingId);
/**
* 작성자 ID로 회의록 목록 조회
*/
List<Minutes> getMinutesByCreator(String createdBy);
/**
* 상태로 회의록 목록 조회
*/
List<Minutes> getMinutesByStatus(String status);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
/**
* 회의록 수정 UseCase
*/
public interface UpdateMinutesUseCase {
/**
* 회의록 제목 수정
*/
Minutes updateMinutesTitle(String minutesId, String title);
/**
* 회의록 버전 증가
*/
Minutes incrementVersion(String minutesId);
}
@@ -0,0 +1,25 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
/**
* 회의록 섹션 생성 UseCase
*/
public interface CreateSectionUseCase {
/**
* 섹션 생성
*/
MinutesSection createSection(CreateSectionCommand command);
/**
* 섹션 생성 명령
*/
record CreateSectionCommand(
String minutesId,
String type,
String title,
String content,
Integer order
) {}
}
@@ -0,0 +1,12 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
/**
* 회의록 섹션 삭제 UseCase
*/
public interface DeleteSectionUseCase {
/**
* 섹션 삭제
*/
void deleteSection(String sectionId);
}
@@ -0,0 +1,26 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
import java.util.List;
/**
* 회의록 섹션 조회 UseCase
*/
public interface GetSectionUseCase {
/**
* ID로 섹션 조회
*/
MinutesSection getSection(String sectionId);
/**
* 회의록 ID로 섹션 목록 조회 (순서대로)
*/
List<MinutesSection> getSectionsByMinutes(String minutesId);
/**
* 회의록 ID와 타입으로 섹션 목록 조회
*/
List<MinutesSection> getSectionsByMinutesAndType(String minutesId, String type);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
/**
* 회의록 섹션 잠금 UseCase
*/
public interface LockSectionUseCase {
/**
* 섹션 잠금
*/
MinutesSection lockSection(String sectionId, String userId);
/**
* 섹션 잠금 해제
*/
MinutesSection unlockSection(String sectionId);
}
@@ -0,0 +1,23 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
/**
* 회의록 섹션 수정 UseCase
*/
public interface UpdateSectionUseCase {
/**
* 섹션 내용 수정
*/
MinutesSection updateSection(UpdateSectionCommand command);
/**
* 섹션 수정 명령
*/
record UpdateSectionCommand(
String sectionId,
String title,
String content
) {}
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
/**
* 회의록 섹션 검증 UseCase
*/
public interface VerifySectionUseCase {
/**
* 섹션 검증
*/
MinutesSection verifySection(String sectionId);
}
@@ -0,0 +1,28 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.template;
import com.unicorn.hgzero.meeting.biz.domain.Template;
import java.util.List;
/**
* 템플릿 생성 UseCase
*/
public interface CreateTemplateUseCase {
/**
* 템플릿 생성
*/
Template createTemplate(CreateTemplateCommand command);
/**
* 템플릿 생성 명령
*/
record CreateTemplateCommand(
String name,
String description,
String category,
List<Template.TemplateSection> sections,
Boolean isPublic,
String createdBy
) {}
}
@@ -0,0 +1,36 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.template;
import com.unicorn.hgzero.meeting.biz.domain.Template;
import java.util.List;
/**
* 템플릿 조회 UseCase
*/
public interface GetTemplateUseCase {
/**
* ID로 템플릿 조회
*/
Template getTemplate(String templateId);
/**
* 카테고리로 템플릿 목록 조회
*/
List<Template> getTemplatesByCategory(String category);
/**
* 공개 템플릿 목록 조회
*/
List<Template> getPublicTemplates();
/**
* 작성자 ID로 템플릿 목록 조회
*/
List<Template> getTemplatesByCreator(String createdBy);
/**
* 이름으로 템플릿 검색
*/
List<Template> searchTemplatesByName(String name);
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
/**
* Todo 취소 UseCase
*/
public interface CancelTodoUseCase {
/**
* Todo 취소
*/
Todo cancelTodo(String todoId);
}
@@ -0,0 +1,14 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
/**
* Todo 완료 UseCase
*/
public interface CompleteTodoUseCase {
/**
* Todo 완료
*/
Todo completeTodo(String todoId);
}
@@ -0,0 +1,29 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import java.time.LocalDate;
/**
* Todo 생성 UseCase
*/
public interface CreateTodoUseCase {
/**
* Todo 생성
*/
Todo createTodo(CreateTodoCommand command);
/**
* Todo 생성 명령
*/
record CreateTodoCommand(
String minutesId,
String meetingId,
String title,
String description,
String assigneeId,
LocalDate dueDate,
String priority
) {}
}
@@ -0,0 +1,42 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import java.time.LocalDate;
import java.util.List;
/**
* Todo 조회 UseCase
*/
public interface GetTodoUseCase {
/**
* ID로 Todo 조회
*/
Todo getTodo(String todoId);
/**
* 회의 ID로 Todo 목록 조회
*/
List<Todo> getTodosByMeeting(String meetingId);
/**
* 회의록 ID로 Todo 목록 조회
*/
List<Todo> getTodosByMinutes(String minutesId);
/**
* 담당자 ID로 Todo 목록 조회
*/
List<Todo> getTodosByAssignee(String assigneeId);
/**
* 담당자 ID와 상태로 Todo 목록 조회
*/
List<Todo> getTodosByAssigneeAndStatus(String assigneeId, String status);
/**
* 담당자 ID와 마감일 범위로 Todo 목록 조회
*/
List<Todo> getTodosByAssigneeAndDueDateRange(String assigneeId, LocalDate startDate, LocalDate endDate);
}
@@ -0,0 +1,28 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import java.time.LocalDate;
/**
* Todo 수정 UseCase
*/
public interface UpdateTodoUseCase {
/**
* Todo 수정
*/
Todo updateTodo(UpdateTodoCommand command);
/**
* Todo 수정 명령
*/
record UpdateTodoCommand(
String todoId,
String title,
String description,
String assigneeId,
LocalDate dueDate,
String priority
) {}
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
/**
* 대시보드 조회 Gateway Interface (Out Port)
*/
public interface DashboardReader {
/**
* 사용자 대시보드 조회
*/
Dashboard getDashboardByUserId(String userId);
/**
* 사용자 대시보드 (기간 필터) 조회
*/
Dashboard getDashboardByUserIdAndPeriod(String userId, String period);
}
@@ -0,0 +1,43 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 회의 조회 Gateway Interface (Out Port)
*/
public interface MeetingReader {
/**
* ID로 회의 조회
*/
Optional<Meeting> findById(String meetingId);
/**
* 주최자 ID로 회의 목록 조회
*/
List<Meeting> findByOrganizerId(String organizerId);
/**
* 상태로 회의 목록 조회
*/
List<Meeting> findByStatus(String status);
/**
* 주최자 ID와 상태로 회의 목록 조회
*/
List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status);
/**
* 일정 시간 범위로 회의 목록 조회
*/
List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
/**
* 템플릿 ID로 회의 목록 조회
*/
List<Meeting> findByTemplateId(String templateId);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
/**
* 회의 저장 Gateway Interface (Out Port)
*/
public interface MeetingWriter {
/**
* 회의 저장
*/
Meeting save(Meeting meeting);
/**
* 회의 삭제
*/
void delete(String meetingId);
}
@@ -0,0 +1,42 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import java.util.List;
import java.util.Optional;
/**
* 회의록 조회 Gateway Interface (Out Port)
*/
public interface MinutesReader {
/**
* ID로 회의록 조회
*/
Optional<Minutes> findById(String minutesId);
/**
* 회의 ID로 회의록 목록 조회
*/
List<Minutes> findByMeetingId(String meetingId);
/**
* 회의 ID로 최신 회의록 조회
*/
Optional<Minutes> findLatestByMeetingId(String meetingId);
/**
* 작성자 ID로 회의록 목록 조회
*/
List<Minutes> findByCreatedBy(String createdBy);
/**
* 상태로 회의록 목록 조회
*/
List<Minutes> findByStatus(String status);
/**
* 확정자 ID로 회의록 목록 조회
*/
List<Minutes> findByFinalizedBy(String finalizedBy);
}
@@ -0,0 +1,42 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
import java.util.List;
import java.util.Optional;
/**
* 회의록 섹션 조회 Gateway Interface (Out Port)
*/
public interface MinutesSectionReader {
/**
* ID로 섹션 조회
*/
Optional<MinutesSection> findById(String sectionId);
/**
* 회의록 ID로 섹션 목록 조회 (순서대로)
*/
List<MinutesSection> findByMinutesIdOrderByOrder(String minutesId);
/**
* 회의록 ID와 타입으로 섹션 목록 조회
*/
List<MinutesSection> findByMinutesIdAndType(String minutesId, String type);
/**
* 회의록 ID와 검증 여부로 섹션 목록 조회
*/
List<MinutesSection> findByMinutesIdAndVerified(String minutesId, Boolean verified);
/**
* 회의록 ID와 잠금 여부로 섹션 목록 조회
*/
List<MinutesSection> findByMinutesIdAndLocked(String minutesId, Boolean locked);
/**
* 잠금한 사용자 ID로 섹션 목록 조회
*/
List<MinutesSection> findByLockedBy(String lockedBy);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
/**
* 회의록 섹션 저장 Gateway Interface (Out Port)
*/
public interface MinutesSectionWriter {
/**
* 섹션 저장
*/
MinutesSection save(MinutesSection section);
/**
* 섹션 삭제
*/
void delete(String sectionId);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
/**
* 회의록 저장 Gateway Interface (Out Port)
*/
public interface MinutesWriter {
/**
* 회의록 저장
*/
Minutes save(Minutes minutes);
/**
* 회의록 삭제
*/
void delete(String minutesId);
}
@@ -0,0 +1,42 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Template;
import java.util.List;
import java.util.Optional;
/**
* 템플릿 조회 Gateway Interface (Out Port)
*/
public interface TemplateReader {
/**
* ID로 템플릿 조회
*/
Optional<Template> findById(String templateId);
/**
* 카테고리로 템플릿 목록 조회
*/
List<Template> findByCategory(String category);
/**
* 공개 여부로 템플릿 목록 조회
*/
List<Template> findByIsPublic(Boolean isPublic);
/**
* 작성자 ID로 템플릿 목록 조회
*/
List<Template> findByCreatedBy(String createdBy);
/**
* 카테고리와 공개 여부로 템플릿 목록 조회
*/
List<Template> findByCategoryAndIsPublic(String category, Boolean isPublic);
/**
* 이름으로 템플릿 검색 (부분 일치)
*/
List<Template> findByNameContaining(String name);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Template;
/**
* 템플릿 저장 Gateway Interface (Out Port)
*/
public interface TemplateWriter {
/**
* 템플릿 저장
*/
Template save(Template template);
/**
* 템플릿 삭제
*/
void delete(String templateId);
}
@@ -0,0 +1,63 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* Todo 조회 Gateway Interface (Out Port)
*/
public interface TodoReader {
/**
* ID로 Todo 조회
*/
Optional<Todo> findById(String todoId);
/**
* 회의 ID로 Todo 목록 조회
*/
List<Todo> findByMeetingId(String meetingId);
/**
* 회의록 ID로 Todo 목록 조회
*/
List<Todo> findByMinutesId(String minutesId);
/**
* 담당자 ID로 Todo 목록 조회
*/
List<Todo> findByAssigneeId(String assigneeId);
/**
* 상태로 Todo 목록 조회
*/
List<Todo> findByStatus(String status);
/**
* 담당자 ID와 상태로 Todo 목록 조회
*/
List<Todo> findByAssigneeIdAndStatus(String assigneeId, String status);
/**
* 마감일로 Todo 목록 조회
*/
List<Todo> findByDueDate(LocalDate dueDate);
/**
* 마감일 이전 Todo 목록 조회
*/
List<Todo> findByDueDateBefore(LocalDate date);
/**
* 우선순위로 Todo 목록 조회
*/
List<Todo> findByPriority(String priority);
/**
* 담당자 ID와 마감일 범위로 Todo 목록 조회
*/
List<Todo> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate);
}
@@ -0,0 +1,19 @@
package com.unicorn.hgzero.meeting.biz.usecase.out;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
/**
* Todo 저장 Gateway Interface (Out Port)
*/
public interface TodoWriter {
/**
* Todo 저장
*/
Todo save(Todo todo);
/**
* Todo 삭제
*/
void delete(String todoId);
}
@@ -0,0 +1,160 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
import com.unicorn.hgzero.meeting.biz.usecase.out.DashboardReader;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 대시보드 Gateway 구현체
* DashboardReader 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DashboardGateway implements DashboardReader {
private final MeetingJpaRepository meetingJpaRepository;
private final MinutesJpaRepository minutesJpaRepository;
private final TodoJpaRepository todoJpaRepository;
@Override
public Dashboard getDashboardByUserId(String userId) {
log.debug("Getting dashboard for user: {}", userId);
// 회의 통계 조회
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).size();
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").size();
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").size();
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").size();
// 회의록 통계 조회
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
.filter(m -> "DRAFT".equals(m.getStatus()))
.count();
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
.filter(m -> "FINALIZED".equals(m.getStatus()))
.count();
// Todo 통계 조회
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
.filter(todo -> todo.getDueDate() != null
&& LocalDate.now().isAfter(todo.getDueDate())
&& !"COMPLETED".equals(todo.getStatus()))
.count();
// 통계 객체 생성
Dashboard.Statistics statistics = new Dashboard.Statistics(
totalMeetings,
scheduledMeetings,
inProgressMeetings,
completedMeetings,
totalMinutes,
draftMinutes,
finalizedMinutes,
totalTodos,
pendingTodos,
completedTodos,
overdueTodos
);
// 대시보드 생성
return Dashboard.builder()
.userId(userId)
.statistics(statistics)
.build();
}
@Override
public Dashboard getDashboardByUserIdAndPeriod(String userId, String period) {
log.debug("Getting dashboard for user: {} with period: {}", userId, period);
// 기간 계산
LocalDateTime startTime = calculateStartTime(period);
LocalDateTime endTime = LocalDateTime.now();
// 기간 내 회의 통계 조회
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).stream()
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
.count();
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").stream()
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
.count();
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").stream()
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
.count();
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").stream()
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
.count();
// 회의록 통계 조회 (전체 기간)
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
.filter(m -> "DRAFT".equals(m.getStatus()))
.count();
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
.filter(m -> "FINALIZED".equals(m.getStatus()))
.count();
// Todo 통계 조회 (전체 기간)
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
.filter(todo -> todo.getDueDate() != null
&& LocalDate.now().isAfter(todo.getDueDate())
&& !"COMPLETED".equals(todo.getStatus()))
.count();
// 통계 객체 생성
Dashboard.Statistics statistics = new Dashboard.Statistics(
totalMeetings,
scheduledMeetings,
inProgressMeetings,
completedMeetings,
totalMinutes,
draftMinutes,
finalizedMinutes,
totalTodos,
pendingTodos,
completedTodos,
overdueTodos
);
// 대시보드 생성
return Dashboard.builder()
.userId(userId)
.period(period)
.statistics(statistics)
.build();
}
/**
* 기간 문자열로부터 시작 시간 계산
*/
private LocalDateTime calculateStartTime(String period) {
LocalDateTime now = LocalDateTime.now();
return switch (period.toUpperCase()) {
case "WEEK" -> now.minusWeeks(1);
case "MONTH" -> now.minusMonths(1);
case "QUARTER" -> now.minusMonths(3);
case "YEAR" -> now.minusYears(1);
default -> now.minusMonths(1); // 기본값: 1개월
};
}
}
@@ -0,0 +1,80 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 회의 Gateway 구현체
* MeetingReader, MeetingWriter 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MeetingGateway implements MeetingReader, MeetingWriter {
private final MeetingJpaRepository meetingJpaRepository;
@Override
public Optional<Meeting> findById(String meetingId) {
return meetingJpaRepository.findById(meetingId)
.map(MeetingEntity::toDomain);
}
@Override
public List<Meeting> findByOrganizerId(String organizerId) {
return meetingJpaRepository.findByOrganizerId(organizerId).stream()
.map(MeetingEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Meeting> findByStatus(String status) {
return meetingJpaRepository.findByStatus(status).stream()
.map(MeetingEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status) {
return meetingJpaRepository.findByOrganizerIdAndStatus(organizerId, status).stream()
.map(MeetingEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
return meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
.map(MeetingEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Meeting> findByTemplateId(String templateId) {
return meetingJpaRepository.findByTemplateId(templateId).stream()
.map(MeetingEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Meeting save(Meeting meeting) {
MeetingEntity entity = MeetingEntity.fromDomain(meeting);
MeetingEntity savedEntity = meetingJpaRepository.save(entity);
return savedEntity.toDomain();
}
@Override
public void delete(String meetingId) {
meetingJpaRepository.deleteById(meetingId);
}
}
@@ -0,0 +1,78 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 회의록 Gateway 구현체
* MinutesReader, MinutesWriter 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MinutesGateway implements MinutesReader, MinutesWriter {
private final MinutesJpaRepository minutesJpaRepository;
@Override
public Optional<Minutes> findById(String minutesId) {
return minutesJpaRepository.findById(minutesId)
.map(MinutesEntity::toDomain);
}
@Override
public List<Minutes> findByMeetingId(String meetingId) {
return minutesJpaRepository.findByMeetingId(meetingId).stream()
.map(MinutesEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Optional<Minutes> findLatestByMeetingId(String meetingId) {
return minutesJpaRepository.findFirstByMeetingIdOrderByVersionDesc(meetingId)
.map(MinutesEntity::toDomain);
}
@Override
public List<Minutes> findByCreatedBy(String createdBy) {
return minutesJpaRepository.findByCreatedBy(createdBy).stream()
.map(MinutesEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Minutes> findByStatus(String status) {
return minutesJpaRepository.findByStatus(status).stream()
.map(MinutesEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Minutes> findByFinalizedBy(String finalizedBy) {
return minutesJpaRepository.findByFinalizedBy(finalizedBy).stream()
.map(MinutesEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Minutes save(Minutes minutes) {
MinutesEntity entity = MinutesEntity.fromDomain(minutes);
MinutesEntity savedEntity = minutesJpaRepository.save(entity);
return savedEntity.toDomain();
}
@Override
public void delete(String minutesId) {
minutesJpaRepository.deleteById(minutesId);
}
}
@@ -0,0 +1,79 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionWriter;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 회의록 섹션 Gateway 구현체
* MinutesSectionReader, MinutesSectionWriter 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MinutesSectionGateway implements MinutesSectionReader, MinutesSectionWriter {
private final MinutesSectionJpaRepository sectionJpaRepository;
@Override
public Optional<MinutesSection> findById(String sectionId) {
return sectionJpaRepository.findById(sectionId)
.map(MinutesSectionEntity::toDomain);
}
@Override
public List<MinutesSection> findByMinutesIdOrderByOrder(String minutesId) {
return sectionJpaRepository.findByMinutesIdOrderByOrderAsc(minutesId).stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<MinutesSection> findByMinutesIdAndType(String minutesId, String type) {
return sectionJpaRepository.findByMinutesIdAndType(minutesId, type).stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<MinutesSection> findByMinutesIdAndVerified(String minutesId, Boolean verified) {
return sectionJpaRepository.findByMinutesIdAndVerified(minutesId, verified).stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<MinutesSection> findByMinutesIdAndLocked(String minutesId, Boolean locked) {
return sectionJpaRepository.findByMinutesIdAndLocked(minutesId, locked).stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<MinutesSection> findByLockedBy(String lockedBy) {
return sectionJpaRepository.findByLockedBy(lockedBy).stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public MinutesSection save(MinutesSection section) {
MinutesSectionEntity entity = MinutesSectionEntity.fromDomain(section);
MinutesSectionEntity savedEntity = sectionJpaRepository.save(entity);
return savedEntity.toDomain();
}
@Override
public void delete(String sectionId) {
sectionJpaRepository.deleteById(sectionId);
}
}
@@ -0,0 +1,79 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Template;
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateWriter;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TemplateEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TemplateJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 템플릿 Gateway 구현체
* TemplateReader, TemplateWriter 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class TemplateGateway implements TemplateReader, TemplateWriter {
private final TemplateJpaRepository templateJpaRepository;
@Override
public Optional<Template> findById(String templateId) {
return templateJpaRepository.findById(templateId)
.map(TemplateEntity::toDomain);
}
@Override
public List<Template> findByCategory(String category) {
return templateJpaRepository.findByCategory(category).stream()
.map(TemplateEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Template> findByIsPublic(Boolean isPublic) {
return templateJpaRepository.findByIsPublic(isPublic).stream()
.map(TemplateEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Template> findByCreatedBy(String createdBy) {
return templateJpaRepository.findByCreatedBy(createdBy).stream()
.map(TemplateEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Template> findByCategoryAndIsPublic(String category, Boolean isPublic) {
return templateJpaRepository.findByCategoryAndIsPublic(category, isPublic).stream()
.map(TemplateEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Template> findByNameContaining(String name) {
return templateJpaRepository.findByNameContaining(name).stream()
.map(TemplateEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Template save(Template template) {
TemplateEntity entity = TemplateEntity.fromDomain(template);
TemplateEntity savedEntity = templateJpaRepository.save(entity);
return savedEntity.toDomain();
}
@Override
public void delete(String templateId) {
templateJpaRepository.deleteById(templateId);
}
}
@@ -0,0 +1,108 @@
package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoWriter;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Todo Gateway 구현체
* TodoReader, TodoWriter 인터페이스 구현
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class TodoGateway implements TodoReader, TodoWriter {
private final TodoJpaRepository todoJpaRepository;
@Override
public Optional<Todo> findById(String todoId) {
return todoJpaRepository.findById(todoId)
.map(TodoEntity::toDomain);
}
@Override
public List<Todo> findByMeetingId(String meetingId) {
return todoJpaRepository.findByMeetingId(meetingId).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByMinutesId(String minutesId) {
return todoJpaRepository.findByMinutesId(minutesId).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByAssigneeId(String assigneeId) {
return todoJpaRepository.findByAssigneeId(assigneeId).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByStatus(String status) {
return todoJpaRepository.findByStatus(status).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByAssigneeIdAndStatus(String assigneeId, String status) {
return todoJpaRepository.findByAssigneeIdAndStatus(assigneeId, status).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByDueDate(LocalDate dueDate) {
return todoJpaRepository.findByDueDate(dueDate).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByDueDateBefore(LocalDate date) {
return todoJpaRepository.findByDueDateBefore(date).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByPriority(String priority) {
return todoJpaRepository.findByPriority(priority).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public List<Todo> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate) {
return todoJpaRepository.findByAssigneeIdAndDueDateBetween(assigneeId, startDate, endDate).stream()
.map(TodoEntity::toDomain)
.collect(Collectors.toList());
}
@Override
public Todo save(Todo todo) {
TodoEntity entity = TodoEntity.fromDomain(todo);
TodoEntity savedEntity = todoJpaRepository.save(entity);
return savedEntity.toDomain();
}
@Override
public void delete(String todoId) {
todoJpaRepository.deleteById(todoId);
}
}
@@ -0,0 +1,112 @@
package com.unicorn.hgzero.meeting.infra.gateway.entity;
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 회의 Entity
*/
@Entity
@Table(name = "meetings")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MeetingEntity extends BaseTimeEntity {
@Id
@Column(name = "meeting_id", length = 50)
private String meetingId;
@Column(name = "title", length = 200, nullable = false)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "scheduled_at", nullable = false)
private LocalDateTime scheduledAt;
@Column(name = "started_at")
private LocalDateTime startedAt;
@Column(name = "ended_at")
private LocalDateTime endedAt;
@Column(name = "status", length = 20, nullable = false)
@Builder.Default
private String status = "SCHEDULED";
@Column(name = "organizer_id", length = 50, nullable = false)
private String organizerId;
@Column(name = "participants", columnDefinition = "TEXT")
private String participants;
@Column(name = "template_id", length = 50)
private String templateId;
public Meeting toDomain() {
return Meeting.builder()
.meetingId(this.meetingId)
.title(this.title)
.description(this.description)
.scheduledAt(this.scheduledAt)
.startedAt(this.startedAt)
.endedAt(this.endedAt)
.status(this.status)
.organizerId(this.organizerId)
.participants(parseParticipants(this.participants))
.templateId(this.templateId)
.build();
}
public static MeetingEntity fromDomain(Meeting meeting) {
return MeetingEntity.builder()
.meetingId(meeting.getMeetingId())
.title(meeting.getTitle())
.description(meeting.getDescription())
.scheduledAt(meeting.getScheduledAt())
.startedAt(meeting.getStartedAt())
.endedAt(meeting.getEndedAt())
.status(meeting.getStatus())
.organizerId(meeting.getOrganizerId())
.participants(formatParticipants(meeting.getParticipants()))
.templateId(meeting.getTemplateId())
.build();
}
public void start() {
this.status = "IN_PROGRESS";
this.startedAt = LocalDateTime.now();
}
public void end() {
this.status = "COMPLETED";
this.endedAt = LocalDateTime.now();
}
private static List<String> parseParticipants(String participants) {
if (participants == null || participants.isEmpty()) {
return List.of();
}
return Arrays.asList(participants.split(","));
}
private static String formatParticipants(List<String> participants) {
if (participants == null || participants.isEmpty()) {
return "";
}
return String.join(",", participants);
}
}
@@ -0,0 +1,96 @@
package com.unicorn.hgzero.meeting.infra.gateway.entity;
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 회의록 Entity
*/
@Entity
@Table(name = "minutes")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MinutesEntity extends BaseTimeEntity {
@Id
@Column(name = "minutes_id", length = 50)
private String minutesId;
@Column(name = "meeting_id", length = 50, nullable = false)
private String meetingId;
@Column(name = "title", length = 200, nullable = false)
private String title;
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("order ASC")
private List<MinutesSectionEntity> sections = new ArrayList<>();
@Column(name = "status", length = 20, nullable = false)
@Builder.Default
private String status = "DRAFT";
@Column(name = "version", nullable = false)
@Builder.Default
private Integer version = 1;
@Column(name = "created_by", length = 50, nullable = false)
private String createdBy;
@Column(name = "finalized_by", length = 50)
private String finalizedBy;
@Column(name = "finalized_at")
private LocalDateTime finalizedAt;
public Minutes toDomain() {
return Minutes.builder()
.minutesId(this.minutesId)
.meetingId(this.meetingId)
.title(this.title)
.sections(this.sections.stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList()))
.status(this.status)
.version(this.version)
.createdBy(this.createdBy)
.finalizedBy(this.finalizedBy)
.finalizedAt(this.finalizedAt)
.build();
}
public static MinutesEntity fromDomain(Minutes minutes) {
return MinutesEntity.builder()
.minutesId(minutes.getMinutesId())
.meetingId(minutes.getMeetingId())
.title(minutes.getTitle())
.status(minutes.getStatus())
.version(minutes.getVersion())
.createdBy(minutes.getCreatedBy())
.finalizedBy(minutes.getFinalizedBy())
.finalizedAt(minutes.getFinalizedAt())
.build();
}
public void finalize(String userId) {
this.status = "FINALIZED";
this.finalizedBy = userId;
this.finalizedAt = LocalDateTime.now();
}
public void updateVersion() {
this.version++;
}
}
@@ -0,0 +1,97 @@
package com.unicorn.hgzero.meeting.infra.gateway.entity;
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 회의록 섹션 Entity
*/
@Entity
@Table(name = "minutes_sections")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MinutesSectionEntity extends BaseTimeEntity {
@Id
@Column(name = "section_id", length = 50)
private String sectionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "minutes_id", nullable = false)
private MinutesEntity minutes;
@Column(name = "minutes_id", insertable = false, updatable = false)
private String minutesId;
@Column(name = "type", length = 50, nullable = false)
private String type;
@Column(name = "title", length = 200, nullable = false)
private String title;
@Column(name = "content", columnDefinition = "TEXT")
private String content;
@Column(name = "order", nullable = false)
private Integer order;
@Column(name = "verified", nullable = false)
@Builder.Default
private Boolean verified = false;
@Column(name = "locked", nullable = false)
@Builder.Default
private Boolean locked = false;
@Column(name = "locked_by", length = 50)
private String lockedBy;
public MinutesSection toDomain() {
return MinutesSection.builder()
.sectionId(this.sectionId)
.minutesId(this.minutesId)
.type(this.type)
.title(this.title)
.content(this.content)
.order(this.order)
.verified(this.verified)
.locked(this.locked)
.lockedBy(this.lockedBy)
.build();
}
public static MinutesSectionEntity fromDomain(MinutesSection section) {
return MinutesSectionEntity.builder()
.sectionId(section.getSectionId())
.minutesId(section.getMinutesId())
.type(section.getType())
.title(section.getTitle())
.content(section.getContent())
.order(section.getOrder())
.verified(section.getVerified())
.locked(section.getLocked())
.lockedBy(section.getLockedBy())
.build();
}
public void lock(String userId) {
this.locked = true;
this.lockedBy = userId;
}
public void unlock() {
this.locked = false;
this.lockedBy = null;
}
public void verify() {
this.verified = true;
}
}
@@ -0,0 +1,65 @@
package com.unicorn.hgzero.meeting.biz.domain;
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 템플릿 Entity
*/
@Entity
@Table(name = "templates")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemplateEntity extends BaseTimeEntity {
@Id
@Column(name = "template_id", length = 50)
private String templateId;
@Column(name = "name", length = 200, nullable = false)
private String name;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "category", length = 50, nullable = false)
private String category;
@Column(name = "sections", columnDefinition = "TEXT")
private String sections;
@Column(name = "is_public", nullable = false)
@Builder.Default
private Boolean isPublic = true;
@Column(name = "created_by", length = 50, nullable = false)
private String createdBy;
public Template toDomain() {
return Template.builder()
.templateId(this.templateId)
.name(this.name)
.description(this.description)
.category(this.category)
.isPublic(this.isPublic)
.createdBy(this.createdBy)
.build();
}
public static TemplateEntity fromDomain(Template template) {
return TemplateEntity.builder()
.templateId(template.getTemplateId())
.name(template.getName())
.description(template.getDescription())
.category(template.getCategory())
.isPublic(template.getIsPublic())
.createdBy(template.getCreatedBy())
.build();
}
}
@@ -0,0 +1,92 @@
package com.unicorn.hgzero.meeting.infra.gateway.entity;
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
import com.unicorn.hgzero.meeting.biz.domain.Todo;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* Todo Entity
*/
@Entity
@Table(name = "todos")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TodoEntity extends BaseTimeEntity {
@Id
@Column(name = "todo_id", length = 50)
private String todoId;
@Column(name = "minutes_id", length = 50)
private String minutesId;
@Column(name = "meeting_id", length = 50, nullable = false)
private String meetingId;
@Column(name = "title", length = 200, nullable = false)
private String title;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "assignee_id", length = 50, nullable = false)
private String assigneeId;
@Column(name = "due_date")
private LocalDate dueDate;
@Column(name = "status", length = 20, nullable = false)
@Builder.Default
private String status = "PENDING";
@Column(name = "priority", length = 20)
@Builder.Default
private String priority = "MEDIUM";
@Column(name = "completed_at")
private LocalDateTime completedAt;
public Todo toDomain() {
return Todo.builder()
.todoId(this.todoId)
.minutesId(this.minutesId)
.meetingId(this.meetingId)
.title(this.title)
.description(this.description)
.assigneeId(this.assigneeId)
.dueDate(this.dueDate)
.status(this.status)
.priority(this.priority)
.completedAt(this.completedAt)
.build();
}
public static TodoEntity fromDomain(Todo todo) {
return TodoEntity.builder()
.todoId(todo.getTodoId())
.minutesId(todo.getMinutesId())
.meetingId(todo.getMeetingId())
.title(todo.getTitle())
.description(todo.getDescription())
.assigneeId(todo.getAssigneeId())
.dueDate(todo.getDueDate())
.status(todo.getStatus())
.priority(todo.getPriority())
.completedAt(todo.getCompletedAt())
.build();
}
public void complete() {
this.status = "COMPLETED";
this.completedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,40 @@
package com.unicorn.hgzero.meeting.infra.gateway.repository;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
* 회의 JPA Repository
*/
@Repository
public interface MeetingJpaRepository extends JpaRepository<MeetingEntity, String> {
/**
* 주최자 ID로 회의 목록 조회
*/
List<MeetingEntity> findByOrganizerId(String organizerId);
/**
* 상태로 회의 목록 조회
*/
List<MeetingEntity> findByStatus(String status);
/**
* 주최자 ID와 상태로 회의 목록 조회
*/
List<MeetingEntity> findByOrganizerIdAndStatus(String organizerId, String status);
/**
* 일정 시간 범위로 회의 목록 조회
*/
List<MeetingEntity> findByScheduledAtBetween(LocalDateTime startTime, LocalDateTime endTime);
/**
* 템플릿 ID로 회의 목록 조회
*/
List<MeetingEntity> findByTemplateId(String templateId);
}
@@ -0,0 +1,45 @@
package com.unicorn.hgzero.meeting.infra.gateway.repository;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 회의록 JPA Repository
*/
@Repository
public interface MinutesJpaRepository extends JpaRepository<MinutesEntity, String> {
/**
* 회의 ID로 회의록 조회
*/
List<MinutesEntity> findByMeetingId(String meetingId);
/**
* 회의 ID로 최신 회의록 조회
*/
Optional<MinutesEntity> findFirstByMeetingIdOrderByVersionDesc(String meetingId);
/**
* 상태로 회의록 목록 조회
*/
List<MinutesEntity> findByStatus(String status);
/**
* 작성자 ID로 회의록 목록 조회
*/
List<MinutesEntity> findByCreatedBy(String createdBy);
/**
* 확정자 ID로 회의록 목록 조회
*/
List<MinutesEntity> findByFinalizedBy(String finalizedBy);
/**
* 회의 ID와 버전으로 회의록 조회
*/
Optional<MinutesEntity> findByMeetingIdAndVersion(String meetingId, Integer version);
}
@@ -0,0 +1,39 @@
package com.unicorn.hgzero.meeting.infra.gateway.repository;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 회의록 섹션 JPA Repository
*/
@Repository
public interface MinutesSectionJpaRepository extends JpaRepository<MinutesSectionEntity, String> {
/**
* 회의록 ID로 섹션 목록 조회 (순서대로)
*/
List<MinutesSectionEntity> findByMinutesIdOrderByOrderAsc(String minutesId);
/**
* 회의록 ID와 타입으로 섹션 목록 조회
*/
List<MinutesSectionEntity> findByMinutesIdAndType(String minutesId, String type);
/**
* 회의록 ID와 검증 여부로 섹션 목록 조회
*/
List<MinutesSectionEntity> findByMinutesIdAndVerified(String minutesId, Boolean verified);
/**
* 회의록 ID와 잠금 여부로 섹션 목록 조회
*/
List<MinutesSectionEntity> findByMinutesIdAndLocked(String minutesId, Boolean locked);
/**
* 잠금한 사용자 ID로 섹션 목록 조회
*/
List<MinutesSectionEntity> findByLockedBy(String lockedBy);
}
@@ -0,0 +1,39 @@
package com.unicorn.hgzero.meeting.infra.gateway.repository;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TemplateEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 템플릿 JPA Repository
*/
@Repository
public interface TemplateJpaRepository extends JpaRepository<TemplateEntity, String> {
/**
* 카테고리로 템플릿 목록 조회
*/
List<TemplateEntity> findByCategory(String category);
/**
* 공개 여부로 템플릿 목록 조회
*/
List<TemplateEntity> findByIsPublic(Boolean isPublic);
/**
* 작성자 ID로 템플릿 목록 조회
*/
List<TemplateEntity> findByCreatedBy(String createdBy);
/**
* 카테고리와 공개 여부로 템플릿 목록 조회
*/
List<TemplateEntity> findByCategoryAndIsPublic(String category, Boolean isPublic);
/**
* 이름으로 템플릿 검색 (부분 일치)
*/
List<TemplateEntity> findByNameContaining(String name);
}
@@ -0,0 +1,60 @@
package com.unicorn.hgzero.meeting.infra.gateway.repository;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
/**
* Todo JPA Repository
*/
@Repository
public interface TodoJpaRepository extends JpaRepository<TodoEntity, String> {
/**
* 회의 ID로 Todo 목록 조회
*/
List<TodoEntity> findByMeetingId(String meetingId);
/**
* 회의록 ID로 Todo 목록 조회
*/
List<TodoEntity> findByMinutesId(String minutesId);
/**
* 담당자 ID로 Todo 목록 조회
*/
List<TodoEntity> findByAssigneeId(String assigneeId);
/**
* 상태로 Todo 목록 조회
*/
List<TodoEntity> findByStatus(String status);
/**
* 담당자 ID와 상태로 Todo 목록 조회
*/
List<TodoEntity> findByAssigneeIdAndStatus(String assigneeId, String status);
/**
* 마감일로 Todo 목록 조회
*/
List<TodoEntity> findByDueDate(LocalDate dueDate);
/**
* 마감일 이전 Todo 목록 조회
*/
List<TodoEntity> findByDueDateBefore(LocalDate date);
/**
* 우선순위로 Todo 목록 조회
*/
List<TodoEntity> findByPriority(String priority);
/**
* 담당자 ID와 마감일 범위로 Todo 목록 조회
*/
List<TodoEntity> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate);
}
@@ -0,0 +1,98 @@
spring:
application:
name: meeting
# Database Configuration
datasource:
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
username: ${DB_USERNAME:hgzerouser}
password: ${DB_PASSWORD:}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
# JPA Configuration
jpa:
show-sql: ${SHOW_SQL:true}
properties:
hibernate:
format_sql: true
use_sql_comments: true
hibernate:
ddl-auto: ${DDL_AUTO:update}
# Redis Configuration
data:
redis:
host: ${REDIS_HOST:20.249.177.114}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
database: ${REDIS_DATABASE:1}
# Server Configuration
server:
port: ${SERVER_PORT:8081}
# JWT Configuration
jwt:
secret: ${JWT_SECRET:}
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
# CORS Configuration
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
# Actuator Configuration
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /actuator
endpoint:
health:
show-details: always
show-components: always
health:
livenessState:
enabled: true
readinessState:
enabled: true
# OpenAPI Documentation
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
show-actuator: false
# Logging Configuration
logging:
level:
com.unicorn.hgzero.meeting: ${LOG_LEVEL_APP:DEBUG}
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
org.springframework.websocket: ${LOG_LEVEL_WEBSOCKET:DEBUG}
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: ${LOG_FILE_PATH:logs/meeting.log}