mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 00:09:10 +00:00
작업 중: Meeting AI 통합 개발 진행 상황 저장
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Minutes Sections 테이블 에러 빠른 해결 가이드
|
||||
|
||||
## 🚨 발생한 에러
|
||||
```
|
||||
Caused by: org.postgresql.util.PSQLException:
|
||||
ERROR: column "id" of relation "minutes_sections" contains null values
|
||||
```
|
||||
|
||||
## ✅ 해결 방법 (2단계)
|
||||
|
||||
### 1단계: 데이터베이스 정리
|
||||
|
||||
IntelliJ에서 다음 중 하나를 실행:
|
||||
|
||||
**방법 A: 직접 SQL 실행**
|
||||
```sql
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
```
|
||||
|
||||
**방법 B: cleanup-minutes-sections.sql 파일 실행**
|
||||
1. IntelliJ Database 탭 열기
|
||||
2. `meetingdb` 우클릭 → New → Query Console
|
||||
3. `cleanup-minutes-sections.sql` 파일 내용 복사 & 실행
|
||||
|
||||
### 2단계: Meeting 서비스 재시작
|
||||
|
||||
IntelliJ Run Configuration에서 Meeting 서비스 재시작
|
||||
|
||||
## 📝 수정된 파일
|
||||
|
||||
1. **test-data-minutes-sections.sql**
|
||||
- Entity 구조에 맞게 컬럼명 수정
|
||||
- `id` 컬럼 추가 (필수)
|
||||
- `type`, `title`, `order` 등 추가
|
||||
- `section_number`, `section_title` 제거
|
||||
|
||||
2. **cleanup-minutes-sections.sql**
|
||||
- null id 레코드 삭제 스크립트
|
||||
|
||||
3. **README-FIX-MINUTES-SECTIONS.md**
|
||||
- 상세 문제 해결 가이드
|
||||
|
||||
## 🔍 확인 사항
|
||||
|
||||
서비스 시작 후 로그 확인:
|
||||
```bash
|
||||
tail -f logs/meeting-service.log
|
||||
```
|
||||
|
||||
에러가 없으면 성공! 다음 단계로 진행하세요.
|
||||
|
||||
## 📚 참고
|
||||
|
||||
- Entity: `MinutesSectionEntity.java`
|
||||
- Repository: `MinutesSectionRepository.java` (필요시 생성)
|
||||
- Service: `EndMeetingService.java`
|
||||
@@ -0,0 +1,72 @@
|
||||
# minutes_sections 테이블 에러 해결 가이드
|
||||
|
||||
## 문제 상황
|
||||
Meeting 서비스 시작 시 다음 에러 발생:
|
||||
```
|
||||
Caused by: org.postgresql.util.PSQLException: ERROR: column "id" of relation "minutes_sections" contains null values
|
||||
```
|
||||
|
||||
## 원인
|
||||
- `minutes_sections` 테이블에 null id를 가진 레코드가 존재
|
||||
- Hibernate가 id 컬럼을 NOT NULL PRIMARY KEY로 변경하려 시도
|
||||
- 기존 null 데이터 때문에 ALTER TABLE 실패
|
||||
|
||||
## 해결 방법
|
||||
|
||||
### 방법 1: IntelliJ Database 도구 사용 (권장)
|
||||
|
||||
1. IntelliJ에서 Database 탭 열기
|
||||
2. `meetingdb` 데이터베이스 연결
|
||||
3. Query Console 열기
|
||||
4. 다음 SQL 실행:
|
||||
|
||||
```sql
|
||||
-- null id를 가진 레코드 삭제
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 결과 확인
|
||||
SELECT COUNT(*) FROM minutes_sections;
|
||||
```
|
||||
|
||||
### 방법 2: cleanup-minutes-sections.sql 파일 실행
|
||||
|
||||
IntelliJ Database Console에서 `cleanup-minutes-sections.sql` 파일을 열어서 실행
|
||||
|
||||
## 실행 후
|
||||
|
||||
1. Meeting 서비스 재시작
|
||||
2. 로그에서 에러가 없는지 확인:
|
||||
```bash
|
||||
tail -f logs/meeting-service.log | grep -i error
|
||||
```
|
||||
3. 정상 시작되면 테스트 진행
|
||||
|
||||
## 추가 정보
|
||||
|
||||
### 테이블 구조 확인
|
||||
```sql
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes_sections'
|
||||
ORDER BY ordinal_position;
|
||||
```
|
||||
|
||||
### 현재 데이터 확인
|
||||
```sql
|
||||
SELECT id, minutes_id, type, title FROM minutes_sections LIMIT 10;
|
||||
```
|
||||
|
||||
### Flyway 마이그레이션 이력 확인
|
||||
```sql
|
||||
SELECT * FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;
|
||||
```
|
||||
|
||||
## 참고사항
|
||||
|
||||
- 이 에러는 기존 테이블에 데이터가 있는 상태에서 Entity 구조가 변경되어 발생
|
||||
- 향후 같은 문제를 방지하려면 Flyway 마이그레이션 파일로 스키마 변경을 관리해야 함
|
||||
- 테스트 데이터는 `test-data-minutes-sections.sql` 파일 참조
|
||||
@@ -0,0 +1,18 @@
|
||||
-- minutes 테이블 구조 확인
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- Primary Key 확인
|
||||
SELECT
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = 'minutes'
|
||||
AND tc.constraint_type = 'PRIMARY KEY';
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# minutes_sections 테이블 정리 스크립트
|
||||
# 목적: null id를 가진 레코드 삭제
|
||||
|
||||
echo "========================================="
|
||||
echo "minutes_sections 테이블 정리 시작"
|
||||
echo "========================================="
|
||||
|
||||
# PostgreSQL 연결 정보
|
||||
DB_HOST="localhost"
|
||||
DB_PORT="5432"
|
||||
DB_NAME="meetingdb"
|
||||
DB_USER="postgres"
|
||||
|
||||
# 1. 기존 데이터 확인
|
||||
echo ""
|
||||
echo "1. 현재 테이블 상태 확인..."
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as total_rows FROM minutes_sections;"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as null_id_rows FROM minutes_sections WHERE id IS NULL;"
|
||||
|
||||
# 2. null id를 가진 레코드 삭제
|
||||
echo ""
|
||||
echo "2. null id를 가진 레코드 삭제..."
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "DELETE FROM minutes_sections WHERE id IS NULL;"
|
||||
|
||||
# 3. 정리 완료 확인
|
||||
echo ""
|
||||
echo "3. 테이블 정리 완료. 현재 상태:"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as remaining_rows FROM minutes_sections;"
|
||||
|
||||
# 4. 테이블 구조 확인
|
||||
echo ""
|
||||
echo "4. 테이블 구조 확인:"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "\d minutes_sections"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "정리 완료! Meeting 서비스를 재시작하세요."
|
||||
echo "========================================="
|
||||
@@ -0,0 +1,26 @@
|
||||
-- ========================================
|
||||
-- minutes_sections 테이블 정리 SQL
|
||||
-- ========================================
|
||||
-- 목적: null id를 가진 레코드 삭제하여 서비스 시작 가능하게 함
|
||||
-- 실행방법: IntelliJ Database 도구에서 실행
|
||||
|
||||
-- 1. 현재 상태 확인
|
||||
SELECT 'Total rows:' as info, COUNT(*) as count FROM minutes_sections
|
||||
UNION ALL
|
||||
SELECT 'Null ID rows:', COUNT(*) FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 2. null id를 가진 레코드 삭제
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 3. 결과 확인
|
||||
SELECT 'Remaining rows:' as info, COUNT(*) as count FROM minutes_sections;
|
||||
|
||||
-- 4. 테이블 구조 확인
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes_sections'
|
||||
ORDER BY ordinal_position;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 직접 실행: minutes_sections 테이블 재생성
|
||||
|
||||
-- 1. 기존 테이블 삭제
|
||||
DROP TABLE IF EXISTS minutes_sections CASCADE;
|
||||
|
||||
-- 2. 테이블 재생성
|
||||
CREATE TABLE minutes_sections (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
minutes_id VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
"order" INTEGER,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
locked BOOLEAN DEFAULT FALSE,
|
||||
locked_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_minutes_sections_minutes
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
|
||||
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
|
||||
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
|
||||
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
|
||||
|
||||
-- 4. 트리거 생성
|
||||
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
|
||||
CREATE TRIGGER update_minutes_sections_updated_at
|
||||
BEFORE UPDATE ON minutes_sections
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 확인
|
||||
SELECT 'minutes_sections 테이블이 성공적으로 생성되었습니다!' as status;
|
||||
+1780
-16013
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+49
-20
@@ -4,18 +4,19 @@ import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
|
||||
import com.unicorn.hgzero.meeting.infra.client.AIServiceClient;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.AgendaSummaryDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ExtractedTodoDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ParticipantMinutesDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
|
||||
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.MinutesSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -39,7 +41,8 @@ import java.util.stream.Collectors;
|
||||
public class EndMeetingService implements EndMeetingUseCase {
|
||||
|
||||
private final MeetingJpaRepository meetingRepository;
|
||||
private final AgendaSectionJpaRepository agendaRepository;
|
||||
private final MinutesJpaRepository minutesRepository;
|
||||
private final MinutesSectionJpaRepository minutesSectionRepository;
|
||||
private final TodoJpaRepository todoRepository;
|
||||
private final MeetingAnalysisJpaRepository analysisRepository;
|
||||
private final AIServiceClient aiServiceClient;
|
||||
@@ -59,13 +62,26 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
||||
|
||||
// 2. 안건 목록 조회 (실제로는 참석자별 메모 섹션)
|
||||
List<AgendaSectionEntity> agendaSections = agendaRepository.findByMeetingIdOrderByAgendaNumberAsc(meetingId);
|
||||
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
|
||||
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
|
||||
|
||||
// 3. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, agendaSections);
|
||||
if (participantMinutesList.isEmpty()) {
|
||||
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
|
||||
}
|
||||
|
||||
// 4. AI Service 호출
|
||||
// 3. 각 회의록의 sections 조회 및 통합
|
||||
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
|
||||
for (MinutesEntity minutes : participantMinutesList) {
|
||||
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
|
||||
minutes.getMinutesId()
|
||||
);
|
||||
allMinutesSections.addAll(sections);
|
||||
}
|
||||
|
||||
// 4. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
|
||||
|
||||
// 5. AI Service 호출
|
||||
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
|
||||
|
||||
// 5. AI 분석 결과 저장
|
||||
@@ -74,25 +90,38 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
// 6. Todo 생성 및 저장
|
||||
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
||||
|
||||
// 7. 회의 종료 처리
|
||||
// 6. 회의 종료 처리
|
||||
meeting.end();
|
||||
meetingRepository.save(meeting);
|
||||
|
||||
// 8. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, agendaSections.size());
|
||||
// 7. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 통합 분석 요청 데이터 생성
|
||||
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
|
||||
*/
|
||||
private ConsolidateRequest createConsolidateRequest(MeetingEntity meeting, List<AgendaSectionEntity> agendaSections) {
|
||||
// 참석자별 회의록 변환 (AgendaSection → ParticipantMinutes)
|
||||
List<ParticipantMinutesDTO> participantMinutes = agendaSections.stream()
|
||||
.<ParticipantMinutesDTO>map(section -> ParticipantMinutesDTO.builder()
|
||||
.userId(section.getMeetingId()) // 실제로는 participantId 필요
|
||||
.userName(section.getAgendaTitle()) // 실제로는 participantName 필요
|
||||
.content(section.getDiscussions() != null ? section.getDiscussions() : "")
|
||||
.build())
|
||||
private ConsolidateRequest createConsolidateRequest(
|
||||
MeetingEntity meeting,
|
||||
List<MinutesSectionEntity> allMinutesSections,
|
||||
List<MinutesEntity> participantMinutesList) {
|
||||
|
||||
// 참석자별 회의록을 ParticipantMinutesDTO로 변환
|
||||
List<ParticipantMinutesDTO> participantMinutes = participantMinutesList.stream()
|
||||
.<ParticipantMinutesDTO>map(minutes -> {
|
||||
// 해당 회의록의 섹션들만 필터링
|
||||
String content = allMinutesSections.stream()
|
||||
.filter(section -> section.getMinutesId().equals(minutes.getMinutesId()))
|
||||
.<String>map(section -> section.getTitle() + "\n" + section.getContent())
|
||||
.collect(Collectors.joining("\n\n"));
|
||||
|
||||
return ParticipantMinutesDTO.builder()
|
||||
.userId(minutes.getUserId())
|
||||
.userName(minutes.getUserId()) // 실제로는 userName이 필요하지만 일단 userId 사용
|
||||
.content(content)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ConsolidateRequest.builder()
|
||||
|
||||
@@ -46,7 +46,7 @@ public class AIServiceClient {
|
||||
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
||||
|
||||
try {
|
||||
String url = aiServiceUrl + "/api/v1/transcripts/consolidate";
|
||||
String url = aiServiceUrl + "/api/transcripts/consolidate";
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
+1
-12
@@ -37,10 +37,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<MinutesSectionEntity> sections = new ArrayList<>();
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT";
|
||||
@@ -64,9 +60,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(this.meetingId)
|
||||
.userId(this.userId)
|
||||
.title(this.title)
|
||||
.sections(this.sections.stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList()))
|
||||
.sections(List.of()) // sections는 별도 조회 필요
|
||||
.status(this.status)
|
||||
.version(this.version)
|
||||
.createdBy(this.createdBy)
|
||||
@@ -83,11 +77,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(minutes.getMeetingId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.sections(minutes.getSections() != null
|
||||
? minutes.getSections().stream()
|
||||
.map(MinutesSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList())
|
||||
: new ArrayList<>())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
|
||||
+19
-24
@@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Entity
|
||||
* 참석자가 작성한 메모를 안건별로 저장
|
||||
* AI 분석의 입력 데이터로 사용됨
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes_sections")
|
||||
@@ -20,43 +22,36 @@ import lombok.NoArgsConstructor;
|
||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "section_id", length = 50)
|
||||
private String sectionId;
|
||||
@Column(name = "id", length = 50)
|
||||
private String id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "minutes_id", nullable = false)
|
||||
private MinutesEntity minutes;
|
||||
|
||||
@Column(name = "minutes_id", insertable = false, updatable = false)
|
||||
@Column(name = "minutes_id", nullable = false, length = 50)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "type", length = 50, nullable = false)
|
||||
@Column(name = "type", length = 50)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
@Column(name = "title", length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "\"order\"")
|
||||
@Builder.Default
|
||||
private Integer order = 0;
|
||||
@Column(name = "order")
|
||||
private Integer order;
|
||||
|
||||
@Column(name = "verified", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean verified = false;
|
||||
@Column(name = "verified")
|
||||
private Boolean verified;
|
||||
|
||||
@Column(name = "locked", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean locked = false;
|
||||
@Column(name = "locked")
|
||||
private Boolean locked;
|
||||
|
||||
@Column(name = "locked_by", length = 50)
|
||||
private String lockedBy;
|
||||
|
||||
public MinutesSection toDomain() {
|
||||
return MinutesSection.builder()
|
||||
.sectionId(this.sectionId)
|
||||
.sectionId(this.id)
|
||||
.minutesId(this.minutesId)
|
||||
.type(this.type)
|
||||
.title(this.title)
|
||||
@@ -70,7 +65,7 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
||||
return MinutesSectionEntity.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.id(section.getSectionId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.type(section.getType())
|
||||
.title(section.getTitle())
|
||||
@@ -82,6 +77,10 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
.build();
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
|
||||
public void lock(String userId) {
|
||||
this.locked = true;
|
||||
this.lockedBy = userId;
|
||||
@@ -91,8 +90,4 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ spring:
|
||||
use_sql_comments: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
ddl-auto: ${JPA_DDL_AUTO:none}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- ========================================
|
||||
-- V5: minutes_sections 테이블 재생성
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-28
|
||||
-- 설명: minutes_sections 테이블을 Entity 구조에 맞게 재생성
|
||||
|
||||
-- 1. 기존 테이블이 있으면 삭제
|
||||
DROP TABLE IF EXISTS minutes_sections CASCADE;
|
||||
|
||||
-- 2. Entity 구조에 맞는 테이블 생성
|
||||
CREATE TABLE minutes_sections (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
minutes_id VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
"order" INTEGER,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
locked BOOLEAN DEFAULT FALSE,
|
||||
locked_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_minutes_sections_minutes
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
|
||||
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
|
||||
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
|
||||
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
|
||||
|
||||
-- 4. 코멘트 추가
|
||||
COMMENT ON TABLE minutes_sections IS '참석자별 회의록 안건 섹션 - AI 통합 회의록 생성 입력 데이터';
|
||||
COMMENT ON COLUMN minutes_sections.id IS '섹션 고유 ID';
|
||||
COMMENT ON COLUMN minutes_sections.minutes_id IS '참석자별 회의록 ID (minutes.id 참조)';
|
||||
COMMENT ON COLUMN minutes_sections.type IS '섹션 타입 (AGENDA: 안건, DISCUSSION: 논의사항, DECISION: 결정사항 등)';
|
||||
COMMENT ON COLUMN minutes_sections.title IS '섹션 제목';
|
||||
COMMENT ON COLUMN minutes_sections.content IS '섹션 내용 (참석자가 작성한 메모)';
|
||||
COMMENT ON COLUMN minutes_sections."order" IS '섹션 순서';
|
||||
COMMENT ON COLUMN minutes_sections.verified IS '검증 완료 여부';
|
||||
COMMENT ON COLUMN minutes_sections.locked IS '편집 잠금 여부';
|
||||
COMMENT ON COLUMN minutes_sections.locked_by IS '잠금 설정한 사용자 ID';
|
||||
|
||||
-- 5. updated_at 자동 업데이트 트리거
|
||||
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
|
||||
CREATE TRIGGER update_minutes_sections_updated_at
|
||||
BEFORE UPDATE ON minutes_sections
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.unicorn.hgzero.meeting.manual;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
/**
|
||||
* 테스트 데이터 삽입 스크립트
|
||||
* 실행: ./gradlew :meeting:bootRun --args='--spring.profiles.active=test'
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.unicorn.hgzero.meeting")
|
||||
public class InsertTestData {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(InsertTestData.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner insertData(JdbcTemplate jdbcTemplate) {
|
||||
return args -> {
|
||||
System.out.println("===== 테스트 데이터 삽입 시작 =====");
|
||||
|
||||
// 1. 참석자 회의록 삽입
|
||||
insertParticipantMinutes(jdbcTemplate);
|
||||
|
||||
// 2. 회의록 섹션 삽입
|
||||
insertMinutesSections(jdbcTemplate);
|
||||
|
||||
// 3. 데이터 확인
|
||||
verifyData(jdbcTemplate);
|
||||
|
||||
System.out.println("===== 테스트 데이터 삽입 완료 =====");
|
||||
};
|
||||
}
|
||||
|
||||
private void insertParticipantMinutes(JdbcTemplate jdbc) {
|
||||
System.out.println("참석자 회의록 삽입 중...");
|
||||
|
||||
String[] inserts = {
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING",
|
||||
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING",
|
||||
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING"
|
||||
};
|
||||
|
||||
for (String sql : inserts) {
|
||||
jdbc.execute(sql);
|
||||
}
|
||||
|
||||
System.out.println("참석자 회의록 삽입 완료");
|
||||
}
|
||||
|
||||
private void insertMinutesSections(JdbcTemplate jdbc) {
|
||||
System.out.println("회의록 섹션 삽입 중...");
|
||||
|
||||
// 참석자 1 섹션
|
||||
insertSection(jdbc, "minutes-user1", 1, "프로젝트 목표 논의",
|
||||
"고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.");
|
||||
insertSection(jdbc, "minutes-user1", 2, "기술 스택 검토",
|
||||
"React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.");
|
||||
|
||||
// 참석자 2 섹션
|
||||
insertSection(jdbc, "minutes-user2", 1, "프로젝트 목표 논의",
|
||||
"성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.");
|
||||
insertSection(jdbc, "minutes-user2", 2, "기술 스택 검토",
|
||||
"캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.");
|
||||
|
||||
// 참석자 3 섹션
|
||||
insertSection(jdbc, "minutes-user3", 1, "프로젝트 목표 논의",
|
||||
"고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.");
|
||||
insertSection(jdbc, "minutes-user3", 2, "기술 스택 검토",
|
||||
"UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.");
|
||||
|
||||
System.out.println("회의록 섹션 삽입 완료");
|
||||
}
|
||||
|
||||
private void insertSection(JdbcTemplate jdbc, String minutesId, int sectionNum, String title, String content) {
|
||||
String sql = "INSERT INTO minutes_sections (minutes_id, section_number, section_title, content, created_at) " +
|
||||
"SELECT id, ?, ?, ?, NOW() FROM minutes WHERE minutes_id = ? " +
|
||||
"ON CONFLICT DO NOTHING";
|
||||
|
||||
jdbc.update(sql, sectionNum, title, content, minutesId);
|
||||
}
|
||||
|
||||
private void verifyData(JdbcTemplate jdbc) {
|
||||
System.out.println("\n===== 데이터 확인 =====");
|
||||
|
||||
Integer minutesCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM minutes WHERE meeting_id = 'meeting-123' AND user_id IS NOT NULL",
|
||||
Integer.class
|
||||
);
|
||||
System.out.println("참석자 회의록 개수: " + minutesCount);
|
||||
|
||||
Integer sectionsCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM minutes_sections ms " +
|
||||
"JOIN minutes m ON ms.minutes_id = m.id " +
|
||||
"WHERE m.meeting_id = 'meeting-123'",
|
||||
Integer.class
|
||||
);
|
||||
System.out.println("회의록 섹션 개수: " + sectionsCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
-- 테스트용 참석자 회의록(minutes) 데이터
|
||||
-- userId가 있는 회의록들 (참석자별 메모)
|
||||
|
||||
-- 참석자 1의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- 참석자 2의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- 참석자 3의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- minutes_sections 데이터 삽입
|
||||
-- Entity 구조에 맞게 수정: id, minutes_id, type, title, content, "order", verified, locked, locked_by
|
||||
|
||||
-- 참석자 1 (홍길동)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user1-1',
|
||||
'minutes-user1',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user1-2',
|
||||
'minutes-user1',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-2'
|
||||
);
|
||||
|
||||
-- 참석자 2 (김철수)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user2-1',
|
||||
'minutes-user2',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user2-2',
|
||||
'minutes-user2',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-2'
|
||||
);
|
||||
|
||||
-- 참석자 3 (이영희)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user3-1',
|
||||
'minutes-user3',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user3-2',
|
||||
'minutes-user3',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-2'
|
||||
);
|
||||
|
||||
-- 확인 쿼리
|
||||
SELECT 'Test data inserted successfully!' as status;
|
||||
SELECT COUNT(*) as minutes_count FROM minutes WHERE meeting_id = 'meeting-123';
|
||||
SELECT COUNT(*) as sections_count FROM minutes_sections;
|
||||
Reference in New Issue
Block a user