mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-01-21 13:46:26 +00:00
Merge branch 'main' of https://github.com/hwanny1128/HGZero
This commit is contained in:
commit
43633fc018
@ -41,6 +41,9 @@ public enum ErrorCode {
|
||||
INVALID_MEETING_TIME(HttpStatus.BAD_REQUEST, "M002", "회의 시작 시간이 종료 시간보다 늦습니다."),
|
||||
MEETING_NOT_FOUND(HttpStatus.NOT_FOUND, "M003", "회의를 찾을 수 없습니다."),
|
||||
INVALID_MEETING_STATUS(HttpStatus.BAD_REQUEST, "M004", "유효하지 않은 회의 상태입니다."),
|
||||
MEETING_NOT_IN_PROGRESS(HttpStatus.BAD_REQUEST, "M005", "회의가 진행 중이 아닙니다."),
|
||||
MINUTES_NOT_FOUND(HttpStatus.NOT_FOUND, "M006", "회의록을 찾을 수 없습니다."),
|
||||
AGENDA_SECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "M007", "안건 섹션을 찾을 수 없습니다."),
|
||||
|
||||
// 외부 시스템 에러 (4xxx)
|
||||
EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "외부 API 호출 중 오류가 발생했습니다."),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -45,21 +45,6 @@ public class AgendaSection {
|
||||
*/
|
||||
private String aiSummaryShort;
|
||||
|
||||
/**
|
||||
* 논의사항 (JSON 형태로 저장)
|
||||
*/
|
||||
private String discussions;
|
||||
|
||||
/**
|
||||
* 결정사항 (JSON 형태로 저장)
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 의견 (JSON 형태로 저장)
|
||||
*/
|
||||
private String opinions;
|
||||
|
||||
/**
|
||||
* 보류사항 (JSON 형태로 저장)
|
||||
*/
|
||||
@ -70,6 +55,11 @@ public class AgendaSection {
|
||||
*/
|
||||
private String todos;
|
||||
|
||||
/**
|
||||
* 요약 (안건 내용)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 생성일시
|
||||
*/
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package com.unicorn.hgzero.meeting.biz.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의 메모 저장 요청")
|
||||
public class MeetingMemoRequest {
|
||||
|
||||
@NotBlank(message = "회의 ID는 필수입니다.")
|
||||
@Schema(description = "회의 ID", example = "meeting-123", required = true)
|
||||
private String meetingId;
|
||||
|
||||
@NotBlank(message = "메모 내용은 필수입니다.")
|
||||
@Schema(description = "메모 내용", example = "[00:15] 신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.", required = true)
|
||||
private String memo;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.meeting.biz.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의 메모 저장 응답")
|
||||
public class MeetingMemoResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "meeting-123")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "저장된 섹션 ID", example = "section-456")
|
||||
private String sectionId;
|
||||
|
||||
@Schema(description = "저장 완료 메시지", example = "메모가 성공적으로 저장되었습니다.")
|
||||
private String message;
|
||||
|
||||
public static MeetingMemoResponse of(String meetingId, String sectionId) {
|
||||
return MeetingMemoResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.sectionId(sectionId)
|
||||
.message("메모가 성공적으로 저장되었습니다.")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
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.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateAgendaSectionsRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -88,4 +91,50 @@ public class AgendaSectionService {
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건 섹션 요약 수정
|
||||
* @param agendaId 안건 ID
|
||||
* @param summary 새로운 요약 내용
|
||||
* @return 수정된 안건 섹션
|
||||
*/
|
||||
@Transactional
|
||||
public AgendaSection updateAgendaSummary(String agendaId, String summary) {
|
||||
log.info("안건 섹션 요약 수정 - agendaId: {}, summary length: {}",
|
||||
agendaId, summary != null ? summary.length() : 0);
|
||||
|
||||
// 먼저 존재 여부 확인
|
||||
AgendaSectionEntity entity = agendaSectionRepository.findById(agendaId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.AGENDA_SECTION_NOT_FOUND,
|
||||
"안건 섹션을 찾을 수 없습니다: " + agendaId));
|
||||
|
||||
// Native Query로 summary만 업데이트
|
||||
agendaSectionRepository.updateSummaryById(agendaId, summary);
|
||||
|
||||
// 업데이트된 엔티티 다시 조회
|
||||
AgendaSectionEntity updatedEntity = agendaSectionRepository.findById(agendaId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.AGENDA_SECTION_NOT_FOUND,
|
||||
"업데이트된 안건 섹션을 찾을 수 없습니다: " + agendaId));
|
||||
|
||||
log.info("안건 섹션 요약 수정 완료 - agendaId: {}", agendaId);
|
||||
return updatedEntity.toDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 안건 섹션의 요약을 한 번에 수정
|
||||
* @param updateItems 안건 ID와 새로운 요약 내용의 목록
|
||||
* @return 수정된 안건 섹션 목록
|
||||
*/
|
||||
@Transactional
|
||||
public List<AgendaSection> updateMultipleAgendaSummaries(
|
||||
List<UpdateAgendaSectionsRequest.AgendaUpdateItem> updateItems) {
|
||||
log.info("안건 섹션 일괄 수정 - 개수: {}", updateItems.size());
|
||||
|
||||
List<AgendaSection> updatedSections = updateItems.stream()
|
||||
.map(item -> updateAgendaSummary(item.getAgendaId(), item.getContent()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("안건 섹션 일괄 수정 완료 - 개수: {}", updatedSections.size());
|
||||
return updatedSections;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
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.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.SaveMeetingMemoUseCase;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionWriter;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
|
||||
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
|
||||
* 회의 중 작성되는 메모를 MinutesSection에 저장하고 관리합니다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingMemoService implements SaveMeetingMemoUseCase {
|
||||
|
||||
private final MeetingReader meetingReader;
|
||||
private final MinutesReader minutesReader;
|
||||
private final MinutesSectionReader minutesSectionReader;
|
||||
private final MinutesSectionWriter minutesSectionWriter;
|
||||
|
||||
/**
|
||||
* 회의 중 AI 메모 저장
|
||||
* @param command 메모 저장 명령
|
||||
* @return 저장된 섹션 ID
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public String saveMemo(SaveMemoCommand command) {
|
||||
log.info("Saving meeting memo for meetingId: {}", command.getMeetingId());
|
||||
|
||||
// 1. 회의 존재 여부 및 진행 중 상태 확인
|
||||
Meeting meeting = meetingReader.findById(command.getMeetingId())
|
||||
.orElseThrow(() -> {
|
||||
log.error("Meeting not found: {}", command.getMeetingId());
|
||||
return new BusinessException(ErrorCode.MEETING_NOT_FOUND);
|
||||
});
|
||||
|
||||
// 2. 회의가 진행 중인지 확인
|
||||
if (!meeting.isInProgress()) {
|
||||
log.warn("Meeting is not in progress: meetingId={}", command.getMeetingId());
|
||||
throw new BusinessException(ErrorCode.MEETING_NOT_IN_PROGRESS);
|
||||
}
|
||||
|
||||
// 3. 해당 회의의 회의록 조회
|
||||
List<Minutes> minutesList = minutesReader.findByMeetingId(command.getMeetingId());
|
||||
if (minutesList.isEmpty()) {
|
||||
log.error("Minutes not found for meetingId: {}", command.getMeetingId());
|
||||
throw new BusinessException(ErrorCode.MINUTES_NOT_FOUND);
|
||||
}
|
||||
Minutes minutes = minutesList.get(0); // 첫 번째 회의록 사용
|
||||
|
||||
// 4. 기존 메모 섹션 조회 또는 새로운 섹션 생성
|
||||
MinutesSection memoSection = minutesSectionReader.findFirstByMinutesIdAndType(
|
||||
minutes.getMinutesId(),
|
||||
"AI_MEMO"
|
||||
).orElseGet(() -> createNewMemoSection(minutes.getMinutesId()));
|
||||
|
||||
// 5. 메모 내용 업데이트 (기존 내용에 추가)
|
||||
String updatedContent = updateMemoContent(
|
||||
memoSection.getContent(),
|
||||
command.getMemo(),
|
||||
command.getUserId()
|
||||
);
|
||||
|
||||
// 6. 섹션 업데이트 및 저장
|
||||
MinutesSection updatedSection = MinutesSection.builder()
|
||||
.sectionId(memoSection.getSectionId())
|
||||
.minutesId(memoSection.getMinutesId())
|
||||
.type("AI_MEMO")
|
||||
.title("회의 메모")
|
||||
.content(updatedContent)
|
||||
.order(memoSection.getOrder())
|
||||
.verified(false)
|
||||
.locked(false)
|
||||
.build();
|
||||
|
||||
minutesSectionWriter.save(updatedSection);
|
||||
|
||||
log.info("Meeting memo saved successfully: sectionId={}", updatedSection.getSectionId());
|
||||
return updatedSection.getSectionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로운 메모 섹션 생성
|
||||
*/
|
||||
private MinutesSection createNewMemoSection(String minutesId) {
|
||||
// 해당 회의록의 최대 order 값 조회
|
||||
int maxOrder = minutesSectionReader.getMaxOrderByMinutesId(minutesId);
|
||||
|
||||
return MinutesSection.builder()
|
||||
.sectionId(generateSectionId())
|
||||
.minutesId(minutesId)
|
||||
.type("AI_MEMO")
|
||||
.title("회의 메모")
|
||||
.content("")
|
||||
.order(maxOrder + 1)
|
||||
.verified(false)
|
||||
.locked(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 내용 업데이트
|
||||
* 기존 내용이 있으면 추가하고, 없으면 새로 작성
|
||||
*/
|
||||
private String updateMemoContent(String existingContent, String newMemo, String userId) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
||||
if (existingContent != null && !existingContent.trim().isEmpty()) {
|
||||
content.append(existingContent);
|
||||
content.append("\n\n");
|
||||
}
|
||||
|
||||
// 타임스탬프와 함께 새로운 메모 추가
|
||||
content.append(String.format("[%s - %s]\n%s",
|
||||
LocalDateTime.now().toString().substring(11, 19), // HH:mm:ss
|
||||
userId,
|
||||
newMemo));
|
||||
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 ID 생성
|
||||
*/
|
||||
private String generateSectionId() {
|
||||
return "section-" + UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
@ -371,6 +371,10 @@ public class MinutesService implements
|
||||
log.warn("섹션 정보 변환 실패 - minutesId: {}", minutes.getMinutesId(), e);
|
||||
}
|
||||
|
||||
// decisions 값 로깅
|
||||
log.info("Minutes decisions 값 확인 - minutesId: {}, decisions: {}",
|
||||
minutes.getMinutesId(), minutes.getDecisions());
|
||||
|
||||
return MinutesDTO.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.meetingId(minutes.getMeetingId())
|
||||
@ -387,6 +391,7 @@ public class MinutesService implements
|
||||
.participantCount(participantCount)
|
||||
.memo("") // 메모 필드는 추후 구현
|
||||
.sections(sectionDTOs) // 섹션 정보 추가
|
||||
.decisions(minutes.getDecisions()) // decisions 필드 추가
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 회의 중 AI 메모 저장 UseCase
|
||||
* 회의 진행 중 사용자가 작성한 메모를 MinutesSection의 content에 저장합니다.
|
||||
*/
|
||||
public interface SaveMeetingMemoUseCase {
|
||||
|
||||
/**
|
||||
* 회의 메모 저장
|
||||
* @param command 메모 저장 명령
|
||||
* @return 저장된 메모의 섹션 ID
|
||||
*/
|
||||
String saveMemo(SaveMemoCommand command);
|
||||
|
||||
/**
|
||||
* 메모 저장 명령
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
class SaveMemoCommand {
|
||||
private final String meetingId;
|
||||
private final String memo;
|
||||
private final String userId;
|
||||
}
|
||||
}
|
||||
@ -39,4 +39,14 @@ public interface MinutesSectionReader {
|
||||
* 잠금한 사용자 ID로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> findByLockedBy(String lockedBy);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 단일 섹션 조회
|
||||
*/
|
||||
Optional<MinutesSection> findFirstByMinutesIdAndType(String minutesId, String type);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 최대 order 값 조회
|
||||
*/
|
||||
int getMaxOrderByMinutesId(String minutesId);
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@ import com.unicorn.hgzero.meeting.infra.dto.response.InviteParticipantResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingEndResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.request.MeetingMemoRequest;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.response.MeetingMemoResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
@ -44,6 +46,7 @@ public class MeetingController {
|
||||
private final CancelMeetingUseCase cancelMeetingUseCase;
|
||||
private final InviteParticipantUseCase inviteParticipantUseCase;
|
||||
private final ApplyTemplateUseCase applyTemplateUseCase;
|
||||
private final SaveMeetingMemoUseCase saveMeetingMemoUseCase;
|
||||
|
||||
/**
|
||||
* 회의 예약
|
||||
@ -319,4 +322,48 @@ public class MeetingController {
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 중 AI 메모 저장
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param userId 사용자 ID
|
||||
* @param userName 사용자명
|
||||
* @param userEmail 사용자 이메일
|
||||
* @param request 메모 저장 요청
|
||||
* @return 메모 저장 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "회의 중 AI 메모 저장",
|
||||
description = "회의 진행 중 사용자가 작성한 메모를 MinutesSection에 저장합니다. AI가 감지한 주요 내용과 사용자가 직접 입력한 내용을 함께 저장할 수 있습니다.",
|
||||
security = @SecurityRequirement(name = "bearerAuth")
|
||||
)
|
||||
@PostMapping("/{meetingId}/memo")
|
||||
public ResponseEntity<ApiResponse<MeetingMemoResponse>> saveMeetingMemo(
|
||||
@Parameter(description = "회의 ID", required = true)
|
||||
@PathVariable String meetingId,
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@Parameter(description = "사용자명", required = true)
|
||||
@RequestHeader("X-User-Name") String userName,
|
||||
@Parameter(description = "사용자 이메일", required = true)
|
||||
@RequestHeader("X-User-Email") String userEmail,
|
||||
@Valid @RequestBody MeetingMemoRequest request) {
|
||||
|
||||
log.info("회의 메모 저장 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
String sectionId = saveMeetingMemoUseCase.saveMemo(
|
||||
SaveMeetingMemoUseCase.SaveMemoCommand.builder()
|
||||
.meetingId(meetingId)
|
||||
.memo(request.getMemo())
|
||||
.userId(userId)
|
||||
.build()
|
||||
);
|
||||
|
||||
var response = MeetingMemoResponse.of(meetingId, sectionId);
|
||||
|
||||
log.info("회의 메모 저장 완료 - meetingId: {}, sectionId: {}", meetingId, sectionId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import com.unicorn.hgzero.meeting.biz.service.MinutesSectionService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.TodoService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.AgendaSectionService;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateMinutesRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateAgendaSectionsRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MinutesDetailResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MinutesListResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.cache.CacheService;
|
||||
@ -186,7 +187,8 @@ public class MinutesController {
|
||||
* PATCH /api/minutes/{minutesId}
|
||||
*/
|
||||
@PatchMapping("/{minutesId}")
|
||||
@Operation(summary = "회의록 수정", description = "회의록 제목과 메모를 수정합니다")
|
||||
@Operation(summary = "회의록 수정 (사용X -> 회의록 안건별 수정 API 사용)",
|
||||
description = "회의록 제목과 메모를 수정합니다. 이 API 대신 회의록 안건별 수정 API(/api/minutes/{minutesId}/agenda-sections)를 사용하세요.")
|
||||
public ResponseEntity<ApiResponse<MinutesDetailResponse>> updateMinutes(
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@RequestHeader("X-User-Name") String userName,
|
||||
@ -415,6 +417,54 @@ public class MinutesController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 안건별 수정
|
||||
* PUT /api/minutes/{minutesId}/agenda-sections
|
||||
*/
|
||||
@PutMapping("/{minutesId}/agenda-sections")
|
||||
@Operation(summary = "회의록 안건별 수정",
|
||||
description = "회의록의 안건별 내용을 수정합니다. 각 안건의 summary 필드만 업데이트됩니다.")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "회의록을 찾을 수 없음")
|
||||
})
|
||||
public ResponseEntity<ApiResponse<String>> updateAgendaSections(
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@RequestHeader(value = "X-User-Name", defaultValue = "System") String userName,
|
||||
@PathVariable String minutesId,
|
||||
@Valid @RequestBody UpdateAgendaSectionsRequest request) {
|
||||
|
||||
log.info("[API] 회의록 안건별 수정 요청 - userId: {}, minutesId: {}, agendaCount: {}",
|
||||
userId, minutesId, request.getAgendas().size());
|
||||
|
||||
try {
|
||||
// 권한 검증 제거 - 모든 사용자가 수정 가능
|
||||
log.info("회의록 수정 권한 검증 스킵 - 모든 사용자 수정 가능");
|
||||
|
||||
// 안건별 내용 수정
|
||||
List<AgendaSection> updatedSections = agendaSectionService.updateMultipleAgendaSummaries(request.getAgendas());
|
||||
|
||||
// 캐시 무효화
|
||||
cacheService.evictCacheMinutesDetail(minutesId);
|
||||
|
||||
log.info("회의록 안건별 수정 성공 - minutesId: {}, updatedCount: {}",
|
||||
minutesId, updatedSections.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("회의록이 성공적으로 수정되었습니다"));
|
||||
|
||||
} catch (BusinessException e) {
|
||||
log.error("회의록 안건별 수정 실패 - 비즈니스 예외 - minutesId: {}", minutesId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("회의록 안건별 수정 실패 - minutesId: {}", minutesId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("회의록 수정에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
/**
|
||||
@ -840,8 +890,10 @@ public class MinutesController {
|
||||
.todoCount(todoProgress.getTotalCount())
|
||||
.build();
|
||||
|
||||
// 결정사항 추출
|
||||
List<MinutesDetailResponse.Decision> decisions = extractDecisions(agendas);
|
||||
// minutes 테이블의 decisions 텍스트 사용
|
||||
String decisions = minutesDTO.getDecisions() != null ? minutesDTO.getDecisions() : "";
|
||||
log.info("Dashboard decisions 값 확인 - minutesId: {}, decisions: {}",
|
||||
minutesDTO.getMinutesId(), decisions);
|
||||
|
||||
// AI 기반 관련회의록 조회 (캐시 우선)
|
||||
List<MinutesDetailResponse.RelatedMinutes> relatedMinutes = getRelatedMinutesFromAI(minutesDTO.getMinutesId());
|
||||
@ -883,29 +935,12 @@ public class MinutesController {
|
||||
|
||||
MinutesSection minutesSection = (MinutesSection) section;
|
||||
|
||||
// AI 요약 정보 구성 (현재는 기본값 사용)
|
||||
MinutesDetailResponse.AiSummary aiSummary = MinutesDetailResponse.AiSummary.builder()
|
||||
.content(minutesSection.getContent() != null ? minutesSection.getContent() : "AI 요약 정보 없음")
|
||||
.generatedAt(LocalDateTime.now().minusMinutes(30))
|
||||
.modifiedAt(LocalDateTime.now().minusMinutes(10))
|
||||
.build();
|
||||
|
||||
// 안건 상세 내용 구성
|
||||
MinutesDetailResponse.AgendaDetails details = MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(parseDiscussions(minutesSection.getContent()))
|
||||
.decisions(parseDecisions(minutesSection.getContent()))
|
||||
.build();
|
||||
|
||||
return MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId(minutesSection.getSectionId())
|
||||
.title(minutesSection.getTitle() != null ? minutesSection.getTitle() : "제목 없음")
|
||||
.orderIndex(minutesSection.getOrder() != null ? minutesSection.getOrder() : 1)
|
||||
.isVerified(minutesSection.isVerified())
|
||||
.verifiedBy(minutesSection.isVerified() ? "시스템" : null)
|
||||
.verifiedAt(minutesSection.isVerified() ? LocalDateTime.now().minusHours(1) : null)
|
||||
.aiSummary(aiSummary)
|
||||
.details(details)
|
||||
.relatedMinutes(new ArrayList<>()) // 관련 회의록은 별도 로직 필요
|
||||
.aiSummary("") // MinutesSection에는 AI 요약이 없음
|
||||
.content(minutesSection.getContent() != null ? minutesSection.getContent() : "")
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -913,67 +948,15 @@ public class MinutesController {
|
||||
* AgendaSection을 AgendaInfo로 변환
|
||||
*/
|
||||
private MinutesDetailResponse.AgendaInfo convertAgendaSectionToAgendaInfo(AgendaSection agendaSection) {
|
||||
// AI 요약 정보 구성
|
||||
MinutesDetailResponse.AiSummary aiSummary = MinutesDetailResponse.AiSummary.builder()
|
||||
.content(agendaSection.getAiSummaryShort() != null ? agendaSection.getAiSummaryShort() : "")
|
||||
.generatedAt(agendaSection.getCreatedAt() != null ? agendaSection.getCreatedAt() : LocalDateTime.now())
|
||||
.modifiedAt(agendaSection.getUpdatedAt() != null ? agendaSection.getUpdatedAt() : LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 안건 상세 내용 구성 - JSON 파싱
|
||||
List<String> discussionsList = parseJsonToList(agendaSection.getDiscussions());
|
||||
List<String> decisionsList = parseJsonToList(agendaSection.getDecisions());
|
||||
|
||||
MinutesDetailResponse.AgendaDetails details = MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(discussionsList)
|
||||
.decisions(decisionsList)
|
||||
.build();
|
||||
|
||||
return MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId(agendaSection.getId())
|
||||
.title(agendaSection.getAgendaTitle() != null ? agendaSection.getAgendaTitle() : "제목 없음")
|
||||
.orderIndex(agendaSection.getAgendaNumber() != null ? agendaSection.getAgendaNumber() : 1)
|
||||
.isVerified(true) // agenda_sections는 기본적으로 검증된 데이터
|
||||
.verifiedBy("AI")
|
||||
.verifiedAt(agendaSection.getCreatedAt())
|
||||
.aiSummary(aiSummary)
|
||||
.details(details)
|
||||
.relatedMinutes(new ArrayList<>()) // 관련 회의록은 별도 로직 필요
|
||||
.aiSummary(agendaSection.getAiSummaryShort() != null ? agendaSection.getAiSummaryShort() : "")
|
||||
.content(agendaSection.getSummary() != null ? agendaSection.getSummary() : "")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List<String>으로 파싱
|
||||
*/
|
||||
private List<String> parseJsonToList(String json) {
|
||||
if (json == null || json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
// 간단한 JSON 배열 파싱
|
||||
json = json.trim();
|
||||
if (json.startsWith("[") && json.endsWith("]")) {
|
||||
json = json.substring(1, json.length() - 1);
|
||||
if (json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 쉼표로 분리하고 따옴표 제거
|
||||
return Arrays.stream(json.split(","))
|
||||
.map(s -> s.trim())
|
||||
.map(s -> s.startsWith("\"") && s.endsWith("\"") ? s.substring(1, s.length() - 1) : s)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// JSON 형식이 아니면 그대로 한 줄로 반환
|
||||
return List.of(json);
|
||||
} catch (Exception e) {
|
||||
log.warn("JSON 파싱 실패: {}", json, e);
|
||||
return List.of(json); // 파싱 실패시 원본 텍스트를 그대로 반환
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private MinutesDetailResponse.SimpleTodo convertToSimpleTodo(Object todo) {
|
||||
@ -1014,10 +997,10 @@ public class MinutesController {
|
||||
List<MinutesDetailResponse.KeyPoint> keyPoints = new ArrayList<>();
|
||||
for (int i = 0; i < agendas.size() && i < 4; i++) {
|
||||
MinutesDetailResponse.AgendaInfo agenda = agendas.get(i);
|
||||
if (agenda.getAiSummary() != null) {
|
||||
if (agenda.getAiSummary() != null && !agenda.getAiSummary().isEmpty()) {
|
||||
keyPoints.add(MinutesDetailResponse.KeyPoint.builder()
|
||||
.index(i + 1)
|
||||
.content(agenda.getAiSummary().getContent())
|
||||
.content(agenda.getAiSummary())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -1030,50 +1013,6 @@ public class MinutesController {
|
||||
return keyPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 섹션 내용에서 논의사항 추출
|
||||
*/
|
||||
private List<String> parseDiscussions(String content) {
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return List.of("논의 내용 없음");
|
||||
}
|
||||
|
||||
// 간단한 패턴으로 논의사항 추출 (실제로는 AI 파싱 필요)
|
||||
Pattern pattern = Pattern.compile("논의[::]\\s*(.+?)(?=결정|\\n|$)", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
List<String> discussions = new ArrayList<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
discussions.add(matcher.group(1).trim());
|
||||
}
|
||||
|
||||
if (discussions.isEmpty()) {
|
||||
// 전체 내용을 논의사항으로 처리
|
||||
discussions.add(content.length() > 100 ? content.substring(0, 100) + "..." : content);
|
||||
}
|
||||
|
||||
return discussions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 섹션 내용에서 결정사항 추출
|
||||
*/
|
||||
private List<String> parseDecisions(String content) {
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 간단한 패턴으로 결정사항 추출 (실제로는 AI 파싱 필요)
|
||||
Pattern pattern = Pattern.compile("결정[::]\\s*(.+?)(?=논의|\\n|$)", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
List<String> decisions = new ArrayList<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
decisions.add(matcher.group(1).trim());
|
||||
}
|
||||
|
||||
return decisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 이름 조회 (실제로는 User 서비스에서 조회 필요)
|
||||
@ -1126,26 +1065,6 @@ public class MinutesController {
|
||||
return List.of("#AI회의록", "#음성인식", "#협업도구", "#스타트업", "#베타출시");
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.Decision> extractDecisions(List<MinutesDetailResponse.AgendaInfo> agendas) {
|
||||
List<MinutesDetailResponse.Decision> decisions = new ArrayList<>();
|
||||
|
||||
for (MinutesDetailResponse.AgendaInfo agenda : agendas) {
|
||||
if (agenda.getDetails() != null && agenda.getDetails().getDecisions() != null) {
|
||||
for (String decision : agenda.getDetails().getDecisions()) {
|
||||
decisions.add(MinutesDetailResponse.Decision.builder()
|
||||
.content(decision)
|
||||
.decidedBy("김민준")
|
||||
.decidedAt(LocalDateTime.now().minusHours(2))
|
||||
.background("안건 논의 결과")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 실제 데이터만 사용
|
||||
|
||||
return decisions;
|
||||
}
|
||||
|
||||
// === 샘플 데이터 생성 메소드들 ===
|
||||
|
||||
@ -1192,9 +1111,9 @@ public class MinutesController {
|
||||
log.debug("AI 분석 결과로 대시보드 정보 업데이트 완료 - minutesId: {}",
|
||||
minutesDTO.getMinutesId());
|
||||
} else {
|
||||
// AI 분석 결과가 없으면 비동기 분석 요청 이벤트 발행
|
||||
publishAiAnalysisRequest(minutesDTO, userId, userName);
|
||||
log.debug("AI 분석 요청 이벤트 발행 완료 - minutesId: {}",
|
||||
// AI 분석 결과가 없으면 비동기 분석 요청 이벤트 발행 - 제거됨
|
||||
// publishAiAnalysisRequest(minutesDTO, userId, userName);
|
||||
log.debug("AI 분석 요청 이벤트 발행 스킵 - DB 값 사용 - minutesId: {}",
|
||||
minutesDTO.getMinutesId());
|
||||
}
|
||||
|
||||
@ -1260,7 +1179,7 @@ public class MinutesController {
|
||||
.keyPoints(keyPoints)
|
||||
.keywords(result.getKeywords() != null ? result.getKeywords() : dashboard.getKeywords())
|
||||
.stats(dashboard.getStats())
|
||||
.decisions(convertAiDecisions(result.getDecisions()))
|
||||
.decisions(dashboard.getDecisions()) // AI 분석에서는 decisions를 변경하지 않음
|
||||
.todoProgress(dashboard.getTodoProgress())
|
||||
.relatedMinutes(convertAiRelatedMinutes(result.getRelatedMinutes()))
|
||||
.build();
|
||||
@ -1289,23 +1208,6 @@ public class MinutesController {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 결정사항을 Response 형식으로 변환
|
||||
*/
|
||||
private List<MinutesDetailResponse.Decision> convertAiDecisions(List<AiAnalysisDTO.Decision> aiDecisions) {
|
||||
if (aiDecisions == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return aiDecisions.stream()
|
||||
.map(decision -> MinutesDetailResponse.Decision.builder()
|
||||
.content(decision.getContent())
|
||||
.decidedBy("AI 분석")
|
||||
.decidedAt(LocalDateTime.now())
|
||||
.background("AI가 회의록에서 추출한 결정사항")
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 관련회의록을 Response 형식으로 변환
|
||||
@ -1329,10 +1231,6 @@ public class MinutesController {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Response 객체 필드 복사 (불변 객체 업데이트용)
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI 분석 요청 이벤트 발행
|
||||
*/
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 안건별 수정 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UpdateAgendaSectionsRequest {
|
||||
|
||||
/**
|
||||
* 수정할 안건 목록
|
||||
*/
|
||||
@NotNull(message = "안건 목록은 필수입니다")
|
||||
private List<AgendaUpdateItem> agendas;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AgendaUpdateItem {
|
||||
/**
|
||||
* 안건 ID (agenda_sections.id)
|
||||
*/
|
||||
@NotBlank(message = "안건 ID는 필수입니다")
|
||||
private String agendaId;
|
||||
|
||||
/**
|
||||
* 안건 내용 (agenda_sections.summary)
|
||||
*/
|
||||
@NotBlank(message = "안건 내용은 필수입니다")
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,7 @@ public class MinutesDetailResponse {
|
||||
private List<KeyPoint> keyPoints; // 핵심내용
|
||||
private List<String> keywords; // 키워드 태그
|
||||
private Statistics stats; // 통계 정보
|
||||
private List<Decision> decisions; // 결정사항
|
||||
private String decisions; // 결정사항 (minutes 테이블의 decisions 텍스트)
|
||||
private TodoProgress todoProgress; // Todo 진행상황
|
||||
private List<RelatedMinutes> relatedMinutes; // 관련회의록
|
||||
}
|
||||
@ -97,16 +97,6 @@ public class MinutesDetailResponse {
|
||||
private int todoCount;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Decision {
|
||||
private String content;
|
||||
private String decidedBy;
|
||||
private LocalDateTime decidedAt;
|
||||
private String background;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@ -155,36 +145,8 @@ public class MinutesDetailResponse {
|
||||
private String agendaId;
|
||||
private String title;
|
||||
private int orderIndex;
|
||||
private boolean isVerified;
|
||||
private String verifiedBy;
|
||||
private LocalDateTime verifiedAt;
|
||||
|
||||
// AI 요약
|
||||
private AiSummary aiSummary;
|
||||
|
||||
// 안건 상세 내용
|
||||
private AgendaDetails details;
|
||||
|
||||
// 관련회의록
|
||||
private List<RelatedMinutes> relatedMinutes;
|
||||
private String aiSummary; // agenda_sections.ai_summary_short
|
||||
private String content; // agenda_sections.summary
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AiSummary {
|
||||
private String content;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AgendaDetails {
|
||||
private List<String> discussions; // 논의 사항
|
||||
private List<String> decisions; // 결정 사항
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,9 @@ 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.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -23,6 +25,7 @@ import java.util.stream.Collectors;
|
||||
public class MinutesSectionGateway implements MinutesSectionReader, MinutesSectionWriter {
|
||||
|
||||
private final MinutesSectionJpaRepository sectionJpaRepository;
|
||||
private final MinutesJpaRepository minutesJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<MinutesSection> findById(String sectionId) {
|
||||
@ -37,7 +40,6 @@ public class MinutesSectionGateway implements MinutesSectionReader, MinutesSecti
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByMinutesIdAndType(String minutesId, String type) {
|
||||
return sectionJpaRepository.findByMinutesIdAndType(minutesId, type).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
@ -73,6 +75,8 @@ public class MinutesSectionGateway implements MinutesSectionReader, MinutesSecti
|
||||
|
||||
if (entity != null) {
|
||||
// 기존 엔티티 업데이트 (minutes 연관관계 유지)
|
||||
entity.updateContent(section.getContent());
|
||||
|
||||
if (section.getLocked() != null && section.getLocked()) {
|
||||
entity.lock(section.getLockedBy());
|
||||
} else if (section.getLocked() != null && !section.getLocked()) {
|
||||
@ -82,7 +86,7 @@ public class MinutesSectionGateway implements MinutesSectionReader, MinutesSecti
|
||||
entity.verify();
|
||||
}
|
||||
} else {
|
||||
// 새 엔티티 생성
|
||||
// 새 엔티티 생성 - minutes 연관관계 설정 필요
|
||||
entity = MinutesSectionEntity.fromDomain(section);
|
||||
}
|
||||
|
||||
@ -90,6 +94,17 @@ public class MinutesSectionGateway implements MinutesSectionReader, MinutesSecti
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MinutesSection> findFirstByMinutesIdAndType(String minutesId, String type) {
|
||||
return sectionJpaRepository.findFirstByMinutesIdAndType(minutesId, type)
|
||||
.map(MinutesSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxOrderByMinutesId(String minutesId) {
|
||||
return sectionJpaRepository.getMaxOrderByMinutesId(minutesId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sectionId) {
|
||||
sectionJpaRepository.deleteById(sectionId);
|
||||
|
||||
@ -3,11 +3,16 @@ package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
/**
|
||||
* 회의 안건 섹션 엔티티
|
||||
* agenda_sections 테이블과 매핑
|
||||
* 안건별 회의록 섹션 Entity
|
||||
* PostgreSQL JSON 컬럼을 사용하여 구조화된 데이터 저장
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "agenda_sections")
|
||||
@ -18,7 +23,7 @@ import lombok.*;
|
||||
public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(length = 36)
|
||||
@Column(name = "id", length = 36)
|
||||
private String id;
|
||||
|
||||
@Column(name = "minutes_id", length = 36, nullable = false)
|
||||
@ -36,23 +41,19 @@ public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
@Column(name = "ai_summary_short", columnDefinition = "TEXT")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Column(name = "discussions", columnDefinition = "TEXT")
|
||||
private String discussions;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "opinions", columnDefinition = "TEXT")
|
||||
private String opinions;
|
||||
|
||||
@Column(name = "pending_items", columnDefinition = "TEXT")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "pending_items", columnDefinition = "json")
|
||||
private String pendingItems;
|
||||
|
||||
@Column(name = "todos", columnDefinition = "TEXT")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "todos", columnDefinition = "json")
|
||||
private String todos;
|
||||
|
||||
@Column(name = "summary", columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 도메인 객체로 변환
|
||||
* Domain 객체로 변환
|
||||
*/
|
||||
public AgendaSection toDomain() {
|
||||
return AgendaSection.builder()
|
||||
@ -62,32 +63,36 @@ public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
.agendaNumber(this.agendaNumber)
|
||||
.agendaTitle(this.agendaTitle)
|
||||
.aiSummaryShort(this.aiSummaryShort)
|
||||
.discussions(this.discussions)
|
||||
.decisions(this.decisions)
|
||||
.opinions(this.opinions)
|
||||
.pendingItems(this.pendingItems)
|
||||
.todos(this.todos)
|
||||
.summary(this.summary)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.updatedAt(this.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인 객체에서 엔티티 생성
|
||||
* Domain 객체에서 Entity 생성
|
||||
*/
|
||||
public static AgendaSectionEntity fromDomain(AgendaSection domain) {
|
||||
public static AgendaSectionEntity fromDomain(AgendaSection section) {
|
||||
return AgendaSectionEntity.builder()
|
||||
.id(domain.getId())
|
||||
.minutesId(domain.getMinutesId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.agendaNumber(domain.getAgendaNumber())
|
||||
.agendaTitle(domain.getAgendaTitle())
|
||||
.aiSummaryShort(domain.getAiSummaryShort())
|
||||
.discussions(domain.getDiscussions())
|
||||
.decisions(domain.getDecisions())
|
||||
.opinions(domain.getOpinions())
|
||||
.pendingItems(domain.getPendingItems())
|
||||
.todos(domain.getTodos())
|
||||
.id(section.getId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.meetingId(section.getMeetingId())
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.pendingItems(section.getPendingItems())
|
||||
.todos(section.getTodos())
|
||||
.summary(section.getSummary())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 내용 업데이트
|
||||
*/
|
||||
public void updateSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
}
|
||||
@ -72,6 +72,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.decisions(this.decisions)
|
||||
.finalizedBy(this.finalizedBy)
|
||||
.finalizedAt(this.finalizedAt)
|
||||
.decisions(this.decisions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -87,6 +88,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.decisions(minutes.getDecisions())
|
||||
.finalizedBy(minutes.getFinalizedBy())
|
||||
.finalizedAt(minutes.getFinalizedAt())
|
||||
.decisions(minutes.getDecisions())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@ -90,4 +90,8 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
public void updateContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -25,4 +28,14 @@ public interface AgendaSectionRepository extends JpaRepository<AgendaSectionEnti
|
||||
* @return 안건 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMeetingIdOrderByAgendaNumber(String meetingId);
|
||||
|
||||
/**
|
||||
* 안건 섹션 요약 업데이트 (Native Query 사용)
|
||||
* @param agendaId 안건 ID
|
||||
* @param summary 요약 내용
|
||||
*/
|
||||
@Modifying
|
||||
@Query(value = "UPDATE agenda_sections SET summary = :summary, updated_at = CURRENT_TIMESTAMP WHERE id = :agendaId",
|
||||
nativeQuery = true)
|
||||
void updateSummaryById(@Param("agendaId") String agendaId, @Param("summary") String summary);
|
||||
}
|
||||
@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 JPA Repository
|
||||
@ -39,4 +40,15 @@ public interface MinutesSectionJpaRepository extends JpaRepository<MinutesSectio
|
||||
* 잠금한 사용자 ID로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSectionEntity> findByLockedBy(String lockedBy);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 단일 섹션 조회
|
||||
*/
|
||||
Optional<MinutesSectionEntity> findFirstByMinutesIdAndType(String minutesId, String type);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 최대 order 값 조회
|
||||
*/
|
||||
@Query("SELECT COALESCE(MAX(m.order), 0) FROM MinutesSectionEntity m WHERE m.minutesId = :minutesId")
|
||||
int getMaxOrderByMinutesId(@Param("minutesId") String minutesId);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user