diff --git a/docs/pr-reviews/PR-12.md b/docs/pr-reviews/PR-12.md new file mode 100644 index 0000000..a0a1e72 --- /dev/null +++ b/docs/pr-reviews/PR-12.md @@ -0,0 +1,35 @@ +# PR-12 AI 리뷰 기록 + +- PR: https://github.com/WhyLog-App/WhyLog/pull/12 +- 제목: feat(server): 서버 db 마이그레이션 설정 & 목데이터 세팅 +- 브랜치: `develop` ← `feat/flyway-migration-mock-data` +- HEAD: `b05eeaa5298c71f1244a6af37f1cf3bc4fe088c3` +- 입력 digest: `5c180ab659adce760e14c2cb4e2945753f9eb527635139536da6e9ef9acc4d84` +- 모델: Google `gemini-3.6-flash` +- 상태: **PASS** +- 생성 시각(UTC): 2026-08-10T21:03:59+00:00 + +## 요약 + +Flyway DB 마이그레이션 도입, 초기 스키마(V1__init_schema.sql) 및 목데이터(mock.sql) 추가, JPA 엔티티 테이블명을 소문자 스네이크 케이스로 일치시키는 작업이 올바르게 반영되었습니다. + +## 이번 실행에서 새로 발견됨 + +없음 + +## 이전 실행부터 계속 남아있음 + +없음 + +## 현재까지 사라짐(자동 추정) + +없음 + +## 실행 이력 + +|HEAD|상태|모델|전체|신규|계속|해결|시각| +|---|---|---|---:|---:|---:|---:|---| +|b05eeaa5298c|PASS|Google gemini-3.6-flash|||||2026-08-10T21:03:59+00:00| + + + diff --git a/server/AGENTS.md b/server/AGENTS.md index 38bc97a..29fa6ae 100644 --- a/server/AGENTS.md +++ b/server/AGENTS.md @@ -4,6 +4,7 @@ - `AGENTS.md` — Server 개발 규칙과 참조 구현 경로 - [`docs/review-checklist.md`](docs/review-checklist.md) — 머지 전 체크리스트. AI 리뷰의 판정 기준으로도 사용합니다. +- [`docs/migrations.md`](docs/migrations.md) — Flyway 마이그레이션·mock.sql 파일명 규칙과 실행 방법 - 새 문서를 추가하거나 경로를 변경하면 이 목차와 루트 `AGENTS.md`의 파트 인덱스를 함께 갱신합니다. 아래 경로 표기는 모두 `src/main/java/com/whylog/server/` 를 기준으로 합니다. diff --git a/server/build.gradle b/server/build.gradle index 2972b73..a46001f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -51,6 +51,8 @@ checkstyle { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-websocket' diff --git a/server/docs/migrations.md b/server/docs/migrations.md new file mode 100644 index 0000000..fb5217d --- /dev/null +++ b/server/docs/migrations.md @@ -0,0 +1,17 @@ +# DB 마이그레이션 · 목데이터 + +## Flyway + +- 스키마는 `src/main/resources/db/migration/`의 Flyway 마이그레이션 파일로만 변경합니다. `spring.jpa.hibernate.ddl-auto`는 `validate`이므로 엔티티만 바꾸고 마이그레이션 파일을 추가하지 않으면 애플리케이션이 기동 시 검증에 실패합니다. +- 파일명은 `V{version}__{설명}.sql` 형식을 씁니다. `version`은 이전 마이그레이션보다 큰 정수(또는 `1.1`처럼 점으로 구분한 정수)를 순서대로 매기고, `설명`은 영문 소문자 snake_case로 씁니다. 예: `V2__add_meeting_summary_status.sql`. +- 버전을 한 번 매긴 파일은 수정하지 않습니다. 이미 실행된 마이그레이션 파일을 고치면 체크섬이 달라져 다음 기동이 실패합니다. 수정이 필요하면 새 버전 파일을 추가합니다. +- Flyway는 DB의 `flyway_schema_history` 테이블에 마지막으로 적용한 버전을 기록해두고, 기동할 때마다 `db/migration`에서 그보다 버전이 큰 파일만 순서대로 찾아 실행합니다. 새 마이그레이션 파일을 추가하고 서버를 재기동하면 자동으로 반영됩니다. +- `spring.flyway.baseline-on-migrate: true`가 켜져 있어, `flyway_schema_history`가 없는 기존 DB(V1 도입 이전에 `ddl-auto: update`로 만들어진 DB)에서도 V1을 기준선으로 잡고 그 이후 마이그레이션만 적용합니다. + +## mock.sql + +- `src/main/resources/db/mock/mock.sql`은 화면 테스트용 목데이터입니다. Flyway 마이그레이션이 아니므로 서버 기동 시 자동 실행되지 않습니다. +- 로컬DB에서 필요할 때 직접 실행합니다. ( DataGrip, Mysql Workbench 등 이용 ) + +- 실행하면 목데이터가 사용하는 테이블을 모두 비우고 고정 ID로 다시 채웁니다. 로컬/dev DB 외에는 실행하지 않습니다. +- 계정 4개(`mock1@gmail.com` ~ `mock4@gmail.com`, 비밀번호 `pwpwpwpw`)로 로그인해 화면을 확인할 수 있습니다. diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/Application.java b/server/src/main/java/com/whylog/server/domain/decision/entity/Application.java index dde8694..6294167 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/Application.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/Application.java @@ -1,7 +1,6 @@ package com.whylog.server.domain.decision.entity; import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -10,17 +9,14 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Table(name = "Application") +@Table(name = "application") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Application extends BaseEntity { @@ -36,14 +32,14 @@ public class Application extends BaseEntity { @Column(name = "name") private String name; -// @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List applicationTimelines = new ArrayList<>(); -// -// @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List applicationCommits = new ArrayList<>(); -// -// @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List applicationBases = new ArrayList<>(); + // @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) + // private final List applicationTimelines = new ArrayList<>(); + // + // @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) + // private final List applicationCommits = new ArrayList<>(); + // + // @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) + // private final List applicationBases = new ArrayList<>(); public static Application create(Decision decision, String name) { Application application = new Application(); diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java index 9f63395..3b51144 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java @@ -1,19 +1,24 @@ package com.whylog.server.domain.decision.entity; import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.*; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Table(name = "Application_Base") +@Table(name = "application_base") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApplicationBase extends BaseEntity { - @EmbeddedId - private ApplicationBaseId id; + @EmbeddedId private ApplicationBaseId id; @MapsId("applicationId") @ManyToOne(fetch = FetchType.LAZY) diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationCommits.java b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationCommits.java index cee6e78..d81a1d4 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationCommits.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationCommits.java @@ -1,19 +1,25 @@ package com.whylog.server.domain.decision.entity; import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Table(name = "Application_Commits") +@Table(name = "application_commits") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApplicationCommits extends BaseEntity { - @EmbeddedId - private ApplicationCommitsId id; + @EmbeddedId private ApplicationCommitsId id; @MapsId("applicationId") @ManyToOne(fetch = FetchType.LAZY) @@ -31,12 +37,14 @@ public class ApplicationCommits extends BaseEntity { @Column(name = "confidence") private Integer confidence; - public static ApplicationCommits create(Application application, - DecisionCommits decisionCommits, - String reason, - Integer confidence) { + public static ApplicationCommits create( + Application application, + DecisionCommits decisionCommits, + String reason, + Integer confidence) { ApplicationCommits applicationCommits = new ApplicationCommits(); - applicationCommits.id = new ApplicationCommitsId(application.getId(), decisionCommits.getId()); + applicationCommits.id = + new ApplicationCommitsId(application.getId(), decisionCommits.getId()); applicationCommits.application = application; applicationCommits.decisionCommits = decisionCommits; applicationCommits.reason = reason; diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java index c315695..fbf9999 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java @@ -14,12 +14,11 @@ @Entity @Getter -@Table(name = "Application_Timeline") +@Table(name = "application_timeline") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApplicationTimeline extends BaseEntity { - @EmbeddedId - private ApplicationTimelineId id; + @EmbeddedId private ApplicationTimelineId id; @MapsId("applicationId") @ManyToOne(fetch = FetchType.LAZY) @@ -31,9 +30,11 @@ public class ApplicationTimeline extends BaseEntity { @JoinColumn(name = "decision_timeline_pk", nullable = false) private DecisionTimeline decisionTimeline; - public static ApplicationTimeline create(Application application, DecisionTimeline decisionTimeline) { + public static ApplicationTimeline create( + Application application, DecisionTimeline decisionTimeline) { ApplicationTimeline applicationTimeline = new ApplicationTimeline(); - applicationTimeline.id = new ApplicationTimelineId(application.getId(), decisionTimeline.getId()); + applicationTimeline.id = + new ApplicationTimelineId(application.getId(), decisionTimeline.getId()); applicationTimeline.application = application; applicationTimeline.decisionTimeline = decisionTimeline; return applicationTimeline; diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/Decision.java b/server/src/main/java/com/whylog/server/domain/decision/entity/Decision.java index e38dd50..b3f36b7 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/Decision.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/Decision.java @@ -21,7 +21,7 @@ @Entity @Getter -@Table(name = "Decision") +@Table(name = "decision") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Decision extends BaseEntity { @@ -42,12 +42,13 @@ public class Decision extends BaseEntity { @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) private final List applications = new ArrayList<>(); -// -// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List decisionTimelines = new ArrayList<>(); -// -// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List decisionBases = new ArrayList<>(); + + // + // @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) + // private final List decisionTimelines = new ArrayList<>(); + // + // @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) + // private final List decisionBases = new ArrayList<>(); public static Decision create(Meeting meeting, boolean isCreated) { Decision decision = new Decision(); diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java index 7e21d74..cc841dc 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java @@ -17,7 +17,7 @@ // 결정근거 @Entity @Getter -@Table(name = "Decision_Base") +@Table(name = "decision_base") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class DecisionBase extends BaseEntity { diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java index 19f0cc4..92257f1 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java @@ -18,12 +18,11 @@ @Entity @Getter @Table( - name = "Decision_Commits", - uniqueConstraints = @UniqueConstraint( - name = "uk_decision_commits_decision_commit", - columnNames = {"decision_id", "commit_id"} - ) -) + name = "decision_commits", + uniqueConstraints = + @UniqueConstraint( + name = "uk_decision_commits_decision_commit", + columnNames = {"decision_id", "commit_id"})) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class DecisionCommits extends BaseEntity { diff --git a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java index aa42c8b..d225003 100644 --- a/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java +++ b/server/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java @@ -16,7 +16,7 @@ @Entity @Getter -@Table(name = "Decision_Timeline") +@Table(name = "decision_timeline") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class DecisionTimeline extends BaseEntity { @@ -48,12 +48,13 @@ public class DecisionTimeline extends BaseEntity { @Column(name = "utterance", columnDefinition = "TEXT") private String utterance; - public static DecisionTimeline create(Decision decision, - String timestamp, - String step, - String content, - Long memberId, - String utterance) { + public static DecisionTimeline create( + Decision decision, + String timestamp, + String step, + String content, + Long memberId, + String utterance) { DecisionTimeline decisionTimeline = new DecisionTimeline(); decisionTimeline.decision = decision; decisionTimeline.timestamp = timestamp; diff --git a/server/src/main/java/com/whylog/server/domain/git/entity/ChangedFile.java b/server/src/main/java/com/whylog/server/domain/git/entity/ChangedFile.java index 0a3cd23..ca622a3 100644 --- a/server/src/main/java/com/whylog/server/domain/git/entity/ChangedFile.java +++ b/server/src/main/java/com/whylog/server/domain/git/entity/ChangedFile.java @@ -16,7 +16,7 @@ @Entity @Getter -@Table(name = "Changed_File") +@Table(name = "changed_file") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ChangedFile extends BaseEntity { diff --git a/server/src/main/java/com/whylog/server/domain/git/entity/Commit.java b/server/src/main/java/com/whylog/server/domain/git/entity/Commit.java index 71ffe37..152da89 100644 --- a/server/src/main/java/com/whylog/server/domain/git/entity/Commit.java +++ b/server/src/main/java/com/whylog/server/domain/git/entity/Commit.java @@ -2,26 +2,35 @@ import com.whylog.server.domain.git.dto.GitRequest; import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Table( - name = "Commits", + name = "commits", uniqueConstraints = { - @UniqueConstraint( - name = "uk_commits_repository_hash", - columnNames = {"repository_id", "hash"} - ) - } -) + @UniqueConstraint( + name = "uk_commits_repository_hash", + columnNames = {"repository_id", "hash"}) + }) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Commit extends BaseEntity { diff --git a/server/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java b/server/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java index 27c3ef8..a32fc1f 100644 --- a/server/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java +++ b/server/src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java @@ -16,7 +16,7 @@ @Entity @Getter -@Table(name = "Commit_Analysis") +@Table(name = "commit_analysis") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommitAnalysis extends BaseEntity { diff --git a/server/src/main/java/com/whylog/server/domain/git/entity/Repository.java b/server/src/main/java/com/whylog/server/domain/git/entity/Repository.java index 4b9eb10..1115dbd 100644 --- a/server/src/main/java/com/whylog/server/domain/git/entity/Repository.java +++ b/server/src/main/java/com/whylog/server/domain/git/entity/Repository.java @@ -22,7 +22,7 @@ @Entity @Getter -@Table(name = "Repository") +@Table(name = "repository") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Repository extends BaseEntity { @@ -55,9 +55,7 @@ public static Repository create(String name, String url, Team team) { return repository; } - /** - * 마지막 동기화 시간을 업데이트합니다. - */ + /** 마지막 동기화 시간을 업데이트합니다. */ public void updateLastSyncedAt(LocalDateTime syncedAt) { this.lastSyncedAt = syncedAt; } diff --git a/server/src/main/java/com/whylog/server/domain/meeting/entity/Dialogue.java b/server/src/main/java/com/whylog/server/domain/meeting/entity/Dialogue.java index 6e3fc2f..3d6b3bc 100644 --- a/server/src/main/java/com/whylog/server/domain/meeting/entity/Dialogue.java +++ b/server/src/main/java/com/whylog/server/domain/meeting/entity/Dialogue.java @@ -18,7 +18,7 @@ @Entity @Getter -@Table(name = "Dialogue") +@Table(name = "dialogue") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Dialogue extends BaseEntity { @@ -41,7 +41,8 @@ public class Dialogue extends BaseEntity { @Column(name = "speech_datetime", nullable = false) private LocalDateTime speechDateTime; - public static Dialogue create(Meeting meeting, Member member, String content, LocalDateTime speechDateTime) { + public static Dialogue create( + Meeting meeting, Member member, String content, LocalDateTime speechDateTime) { Dialogue dialogue = new Dialogue(); dialogue.meeting = meeting; dialogue.member = member; diff --git a/server/src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java b/server/src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java index 99b50a7..028e967 100644 --- a/server/src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java +++ b/server/src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java @@ -1,26 +1,37 @@ package com.whylog.server.domain.meeting.entity; -import com.whylog.server.domain.decision.entity.Decision; import com.whylog.server.domain.meeting.dto.MeetingRequest; import com.whylog.server.domain.meeting.enums.MeetingStatus; import com.whylog.server.domain.team.entity.Team; import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.*; - +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; import java.time.Duration; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; - -import lombok.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter -@Table(name = "Meeting") +@Table(name = "meeting") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class -Meeting extends BaseEntity { +public class Meeting extends BaseEntity { @Id @Column(name = "meeting_id") @@ -61,17 +72,14 @@ private Meeting(String name, Team team) { } public static Meeting create(MeetingRequest.MeetingCreateDTO dto, Team team) { - return Meeting.builder() - .name(dto.getName()) - .team(team) - .build(); + return Meeting.builder().name(dto.getName()).team(team).build(); } - public MeetingStatus getStatus(){ + public MeetingStatus getStatus() { return this.endDateTime == null ? MeetingStatus.ONGOING : MeetingStatus.COMPLETED; } - public boolean isOngoing(){ + public boolean isOngoing() { return this.endDateTime == null; } @@ -82,7 +90,7 @@ public LocalDateTime endMeeting() { } public Long getDuration() { - if(this.endDateTime == null) return null; + if (this.endDateTime == null) return null; return ChronoUnit.MINUTES.between(startDateTime, endDateTime); } @@ -101,15 +109,18 @@ public String getElapse() { @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) private final List meetingMembers = new ArrayList<>(); -// + + // @OneToOne(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) private MeetingAnalysis meetingAnalysis; -// + + // @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) private final List dialogues = new ArrayList<>(); -// -// @OneToOne(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) -// private Decision decision; + + // + // @OneToOne(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true) + // private Decision decision; public void attachMeetingAnalysis(MeetingAnalysis meetingAnalysis) { this.meetingAnalysis = meetingAnalysis; diff --git a/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingAnalysis.java b/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingAnalysis.java index c535dc4..6f559ff 100644 --- a/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingAnalysis.java +++ b/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingAnalysis.java @@ -2,15 +2,24 @@ import com.whylog.server.global.entity.BaseEntity; import com.whylog.server.global.util.json.StringListJsonConverter; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; @Entity @Getter -@Table(name = "Meeting_Analysis") +@Table(name = "meeting_analysis") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class MeetingAnalysis extends BaseEntity { @@ -85,7 +94,5 @@ public record MeetingAnalysisPayload( List topics, List coreContext, List applicationTitles, - List applicationReasons - ) { - } + List applicationReasons) {} } diff --git a/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingMember.java b/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingMember.java index 15a2f07..69a859c 100644 --- a/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingMember.java +++ b/server/src/main/java/com/whylog/server/domain/meeting/entity/MeetingMember.java @@ -12,19 +12,18 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.MapsId; import jakarta.persistence.Table; -import lombok.Builder; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Table(name = "Meeting_Member") +@Table(name = "meeting_member") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class MeetingMember extends BaseEntity { - @EmbeddedId - private MeetingMemberId id; + @EmbeddedId private MeetingMemberId id; @MapsId("meetingId") @ManyToOne(fetch = FetchType.LAZY) diff --git a/server/src/main/java/com/whylog/server/domain/team/entity/Team.java b/server/src/main/java/com/whylog/server/domain/team/entity/Team.java index 5c8a7e3..899f654 100644 --- a/server/src/main/java/com/whylog/server/domain/team/entity/Team.java +++ b/server/src/main/java/com/whylog/server/domain/team/entity/Team.java @@ -21,7 +21,7 @@ @Entity @Getter -@Table(name = "Team") +@Table(name = "team") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Team extends BaseEntity { @@ -43,18 +43,17 @@ private Team(String name, String image) { } public static Team create(TeamRequest.TeamCreateDTO dto, String image) { - return Team.builder() - .name(dto.getName()) - .image(image) - .build(); + return Team.builder().name(dto.getName()).image(image).build(); } @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) private final List teamMembers = new ArrayList<>(); -// + + // @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) private final List meetings = new ArrayList<>(); -// + + // @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) private final List repositories = new ArrayList<>(); } diff --git a/server/src/main/java/com/whylog/server/domain/team/entity/TeamMember.java b/server/src/main/java/com/whylog/server/domain/team/entity/TeamMember.java index 87fc26c..440087f 100644 --- a/server/src/main/java/com/whylog/server/domain/team/entity/TeamMember.java +++ b/server/src/main/java/com/whylog/server/domain/team/entity/TeamMember.java @@ -20,12 +20,11 @@ @Entity @Getter -@Table(name = "Team_Member") +@Table(name = "team_member") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TeamMember extends BaseEntity { - @EmbeddedId - private TeamMemberId id; + @EmbeddedId private TeamMemberId id; @MapsId("teamId") @ManyToOne(fetch = FetchType.LAZY) diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 64eaeb8..ae82619 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -25,7 +25,7 @@ spring: jpa: open-in-view: false hibernate: - ddl-auto: update + ddl-auto: validate show-sql: true properties: hibernate: @@ -33,6 +33,11 @@ spring: format_sql: true jdbc.time_zone: Asia/Seoul + flyway: + enabled: true + locations: classpath:db/migration + baseline-on-migrate: true + data: redis: host: ${DEV_REDIS_HOST} diff --git a/server/src/main/resources/db/migration/V1__init_schema.sql b/server/src/main/resources/db/migration/V1__init_schema.sql new file mode 100644 index 0000000..195b950 --- /dev/null +++ b/server/src/main/resources/db/migration/V1__init_schema.sql @@ -0,0 +1,300 @@ +create table member ( + member_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + name varchar(50) not null, + email varchar(50) not null, + password varchar(255) not null, + profile_image varchar(255), + role enum ('USER') not null, + github_access_token varchar(500), + primary key (member_id) +) engine=InnoDB; + +create table team ( + team_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + name varchar(50) not null, + image varchar(255), + primary key (team_id) +) engine=InnoDB; + +create table team_member ( + team_id bigint not null, + member_id bigint not null, + created_at datetime(6), + updated_at datetime(6), + is_active bit, + role enum ('MEMBER','OWNER'), + primary key (team_id, member_id) +) engine=InnoDB; + +create table repository ( + repository_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + name varchar(255) not null, + url varchar(500) not null, + last_synced_at datetime(6), + team_id bigint not null, + primary key (repository_id) +) engine=InnoDB; + +create table commits ( + commit_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + repository_id bigint not null, + hash varchar(255) not null, + message TEXT not null, + author_name varchar(50) not null, + author_email varchar(50) not null, + author_profile_image varchar(255) not null, + datetime datetime(6) not null, + added_lines integer not null, + deleted_lines integer not null, + primary key (commit_id) +) engine=InnoDB; + +create table changed_file ( + changed_file_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + commit_id bigint not null, + file_name varchar(255) not null, + primary key (changed_file_id) +) engine=InnoDB; + +create table commit_analysis ( + commit_Analysis_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + commit_id bigint not null, + summary TEXT, + embedding_ready bit not null, + primary key (commit_Analysis_id) +) engine=InnoDB; + +create table meeting ( + meeting_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + team_id bigint not null, + name varchar(50), + start_date_time datetime(6) not null, + end_date_time datetime(6), + audio_key varchar(255), + audio_egress_id varchar(100), + is_normally_ended bit not null, + primary key (meeting_id) +) engine=InnoDB; + +create table meeting_member ( + meeting_id bigint not null, + member_id bigint not null, + created_at datetime(6), + updated_at datetime(6), + role enum ('GENERAL','OWNER'), + primary key (meeting_id, member_id) +) engine=InnoDB; + +create table meeting_analysis ( + meeting_analysis_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + meeting_id bigint not null, + meeting_title varchar(255), + meeting_purpose varchar(255), + meeting_duration varchar(255), + analysis_content LONGTEXT, + topics LONGTEXT, + core_context LONGTEXT, + application_titles LONGTEXT, + application_reasons LONGTEXT, + primary key (meeting_analysis_id) +) engine=InnoDB; + +create table dialogue ( + dialogue_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + meeting_id bigint not null, + member_id bigint not null, + content TEXT not null, + speech_datetime datetime(6) not null, + primary key (dialogue_id) +) engine=InnoDB; + +create table decision ( + decision_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + meeting_id bigint not null, + is_created bit, + reliability_score integer, + primary key (decision_id) +) engine=InnoDB; + +create table decision_base ( + decision_base_pk bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + decision_id bigint not null, + content TEXT, + primary key (decision_base_pk) +) engine=InnoDB; + +create table decision_commits ( + decision_commits_pk bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + decision_id bigint not null, + commit_id bigint not null, + primary key (decision_commits_pk) +) engine=InnoDB; + +create table decision_timeline ( + decision_timeline_pk bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + decision_id bigint not null, + timestamp varchar(30), + step varchar(10), + content TEXT, + member_id bigint, + utterance TEXT, + primary key (decision_timeline_pk) +) engine=InnoDB; + +create table application ( + application_id bigint not null auto_increment, + created_at datetime(6), + updated_at datetime(6), + decision_id bigint not null, + name varchar(255), + primary key (application_id) +) engine=InnoDB; + +create table application_base ( + application_id bigint not null, + decision_base_pk bigint not null, + created_at datetime(6), + updated_at datetime(6), + primary key (application_id, decision_base_pk) +) engine=InnoDB; + +create table application_commits ( + application_id bigint not null, + decision_commits_pk bigint not null, + created_at datetime(6), + updated_at datetime(6), + reason TEXT, + confidence integer, + primary key (application_id, decision_commits_pk) +) engine=InnoDB; + +create table application_timeline ( + application_id bigint not null, + decision_timeline_pk bigint not null, + created_at datetime(6), + updated_at datetime(6), + primary key (application_id, decision_timeline_pk) +) engine=InnoDB; + +create table commit_connection ( + application_id bigint not null, + commit_id bigint not null, + created_at datetime(6), + updated_at datetime(6), + primary key (application_id, commit_id) +) engine=InnoDB; + +alter table member + add constraint uk_member_email unique (email); + +alter table team + add constraint uk_team_name unique (name); + +alter table commits + add constraint uk_commits_repository_hash unique (repository_id, hash); + +alter table commit_analysis + add constraint uk_commit_analysis_commit unique (commit_id); + +alter table decision + add constraint uk_decision_meeting unique (meeting_id); + +alter table decision_commits + add constraint uk_decision_commits_decision_commit unique (decision_id, commit_id); + +alter table meeting_analysis + add constraint uk_meeting_analysis_meeting unique (meeting_id); + +alter table team_member + add constraint fk_team_member_team foreign key (team_id) references team (team_id); +alter table team_member + add constraint fk_team_member_member foreign key (member_id) references member (member_id); + +alter table repository + add constraint fk_repository_team foreign key (team_id) references team (team_id); + +alter table commits + add constraint fk_commits_repository foreign key (repository_id) references repository (repository_id); + +alter table changed_file + add constraint fk_changed_file_commit foreign key (commit_id) references commits (commit_id); + +alter table commit_analysis + add constraint fk_commit_analysis_commit foreign key (commit_id) references commits (commit_id); + +alter table meeting + add constraint fk_meeting_team foreign key (team_id) references team (team_id); + +alter table meeting_member + add constraint fk_meeting_member_meeting foreign key (meeting_id) references meeting (meeting_id); +alter table meeting_member + add constraint fk_meeting_member_member foreign key (member_id) references member (member_id); + +alter table meeting_analysis + add constraint fk_meeting_analysis_meeting foreign key (meeting_id) references meeting (meeting_id); + +alter table dialogue + add constraint fk_dialogue_meeting foreign key (meeting_id) references meeting (meeting_id); +alter table dialogue + add constraint fk_dialogue_member foreign key (member_id) references member (member_id); + +alter table decision + add constraint fk_decision_meeting foreign key (meeting_id) references meeting (meeting_id); + +alter table decision_base + add constraint fk_decision_base_decision foreign key (decision_id) references decision (decision_id); + +alter table decision_commits + add constraint fk_decision_commits_decision foreign key (decision_id) references decision (decision_id); + +alter table decision_timeline + add constraint fk_decision_timeline_decision foreign key (decision_id) references decision (decision_id); + +alter table application + add constraint fk_application_decision foreign key (decision_id) references decision (decision_id); + +alter table application_base + add constraint fk_application_base_application foreign key (application_id) references application (application_id); +alter table application_base + add constraint fk_application_base_decision_base foreign key (decision_base_pk) references decision_base (decision_base_pk); + +alter table application_commits + add constraint fk_application_commits_application foreign key (application_id) references application (application_id); +alter table application_commits + add constraint fk_application_commits_decision_commits foreign key (decision_commits_pk) references decision_commits (decision_commits_pk); + +alter table application_timeline + add constraint fk_application_timeline_application foreign key (application_id) references application (application_id); +alter table application_timeline + add constraint fk_application_timeline_decision_timeline foreign key (decision_timeline_pk) references decision_timeline (decision_timeline_pk); + +alter table commit_connection + add constraint fk_commit_connection_application foreign key (application_id) references application (application_id); +alter table commit_connection + add constraint fk_commit_connection_commit foreign key (commit_id) references commits (commit_id); diff --git a/server/src/main/resources/db/mock/mock.sql b/server/src/main/resources/db/mock/mock.sql new file mode 100644 index 0000000..5f41dd1 --- /dev/null +++ b/server/src/main/resources/db/mock/mock.sql @@ -0,0 +1,191 @@ +-- 로컬/dev DB 화면 테스트용 목데이터. +-- Flyway가 자동 실행하지 않는다. 필요할 때 mysql 클라이언트 등으로 직접 실행한다. +-- mysql -h -u -p < db/mock/mock.sql +-- 모든 목데이터 테이블을 비우고 고정 ID로 다시 채우므로, 로컬/dev DB 외에는 실행하지 않는다. +-- 계정 4개는 mock1@gmail.com ~ mock4@gmail.com, 비밀번호는 모두 pwpwpwpw (BCrypt 인코딩). + +SET FOREIGN_KEY_CHECKS = 0; + +TRUNCATE TABLE commit_connection; +TRUNCATE TABLE application_timeline; +TRUNCATE TABLE application_commits; +TRUNCATE TABLE application_base; +TRUNCATE TABLE application; +TRUNCATE TABLE decision_timeline; +TRUNCATE TABLE decision_commits; +TRUNCATE TABLE decision_base; +TRUNCATE TABLE decision; +TRUNCATE TABLE meeting_analysis; +TRUNCATE TABLE dialogue; +TRUNCATE TABLE meeting_member; +TRUNCATE TABLE meeting; +TRUNCATE TABLE commit_analysis; +TRUNCATE TABLE changed_file; +TRUNCATE TABLE commits; +TRUNCATE TABLE repository; +TRUNCATE TABLE team_member; +TRUNCATE TABLE team; +TRUNCATE TABLE member; + +SET FOREIGN_KEY_CHECKS = 1; + +-- member (비밀번호: pwpwpwpw) +INSERT INTO member (member_id, created_at, updated_at, name, email, password, role) VALUES + (1, NOW(), NOW(), '목데이터1', 'mock1@gmail.com', '$2a$10$dnlVdvAaeUpcta/kmlVfO.bJfuauckS1ne6j6sSjyUa8j.BOsDTL2', 'USER'), + (2, NOW(), NOW(), '목데이터2', 'mock2@gmail.com', '$2a$10$dnlVdvAaeUpcta/kmlVfO.bJfuauckS1ne6j6sSjyUa8j.BOsDTL2', 'USER'), + (3, NOW(), NOW(), '목데이터3', 'mock3@gmail.com', '$2a$10$dnlVdvAaeUpcta/kmlVfO.bJfuauckS1ne6j6sSjyUa8j.BOsDTL2', 'USER'), + (4, NOW(), NOW(), '목데이터4', 'mock4@gmail.com', '$2a$10$dnlVdvAaeUpcta/kmlVfO.bJfuauckS1ne6j6sSjyUa8j.BOsDTL2', 'USER'); + +-- team +INSERT INTO team (team_id, created_at, updated_at, name, image) VALUES + (1, NOW(), NOW(), 'WhyLog 테스트팀', NULL); + +INSERT INTO team_member (team_id, member_id, created_at, updated_at, is_active, role) VALUES + (1, 1, NOW(), NOW(), 1, 'OWNER'), + (1, 2, NOW(), NOW(), 1, 'MEMBER'), + (1, 3, NOW(), NOW(), 1, 'MEMBER'), + (1, 4, NOW(), NOW(), 1, 'MEMBER'); + +-- repository & commits (10건) +INSERT INTO repository (repository_id, created_at, updated_at, name, url, last_synced_at, team_id) VALUES + (1, NOW(), NOW(), 'whylog-server', 'https://github.com/WhyLog-App/whylog-server', NOW(), 1); + +INSERT INTO commits (commit_id, created_at, updated_at, repository_id, hash, message, author_name, author_email, author_profile_image, datetime, added_lines, deleted_lines) VALUES + (1, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000001', 'feat: 로그인 기능 추가', '목데이터1', 'mock1@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 5 DAY), 120, 5), + (2, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000002', 'fix: 회의 종료 시간 버그 수정', '목데이터2', 'mock2@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 5 DAY), 30, 12), + (3, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000003', 'refactor: 결정 근거 조회 쿼리 정리', '목데이터3', 'mock3@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 4 DAY), 60, 40), + (4, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000004', 'feat: 커밋 분석 임베딩 파이프라인 추가', '목데이터4', 'mock4@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 3 DAY), 200, 10), + (5, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000005', 'fix: JWT 리프레시 토큰 만료 처리 수정', '목데이터1', 'mock1@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 3 DAY), 45, 20), + (6, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000006', 'feat: 팀 초대 API 추가', '목데이터2', 'mock2@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 2 DAY), 90, 3), + (7, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000007', 'perf: 회의 요약 캐싱 적용', '목데이터3', 'mock3@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 2 DAY), 70, 15), + (8, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000008', 'fix: GitHub 웹훅 서명 검증 버그 수정', '목데이터4', 'mock4@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 1 DAY), 25, 8), + (9, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000009', 'chore: Flyway 마이그레이션 도입', '목데이터1', 'mock1@gmail.com', 'https://placehold.co/64x64', DATE_SUB(NOW(), INTERVAL 1 DAY), 350, 0), + (10, NOW(), NOW(), 1, 'a1b2c3d4e5f60000000000000000000000000010', 'docs: PR 리뷰 기록 문서화', '목데이터2', 'mock2@gmail.com', 'https://placehold.co/64x64', NOW(), 80, 2); + +INSERT INTO changed_file (changed_file_id, created_at, updated_at, commit_id, file_name) VALUES + (1, NOW(), NOW(), 1, 'src/main/java/com/whylog/server/domain/user/service/LocalLoginService.java'), + (2, NOW(), NOW(), 1, 'src/main/java/com/whylog/server/domain/user/controller/AuthController.java'), + (3, NOW(), NOW(), 2, 'src/main/java/com/whylog/server/domain/meeting/entity/Meeting.java'), + (4, NOW(), NOW(), 3, 'src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java'), + (5, NOW(), NOW(), 4, 'src/main/java/com/whylog/server/domain/git/service/CommitAnalysisService.java'), + (6, NOW(), NOW(), 4, 'src/main/java/com/whylog/server/domain/git/entity/CommitAnalysis.java'), + (7, NOW(), NOW(), 5, 'src/main/java/com/whylog/server/global/util/jwt/JwtProvider.java'), + (8, NOW(), NOW(), 6, 'src/main/java/com/whylog/server/domain/team/service/TeamCommandService.java'), + (9, NOW(), NOW(), 7, 'src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java'), + (10, NOW(), NOW(), 8, 'src/main/java/com/whylog/server/global/external/github/GithubWebhookVerifier.java'), + (11, NOW(), NOW(), 9, 'src/main/resources/db/migration/V1__init_schema.sql'), + (12, NOW(), NOW(), 10, 'docs/pr-reviews/2026-08.md'); + +INSERT INTO commit_analysis (commit_Analysis_id, created_at, updated_at, commit_id, summary, embedding_ready) VALUES + (1, NOW(), NOW(), 1, '로그인 API에 이메일/비밀번호 검증 로직을 추가했다.', 1), + (2, NOW(), NOW(), 3, '결정 근거 조회 시 N+1이 발생하던 쿼리를 fetch join으로 정리했다.', 1), + (3, NOW(), NOW(), 4, '커밋 메시지를 벡터로 변환해 저장하는 임베딩 파이프라인을 추가했다.', 1), + (4, NOW(), NOW(), 7, '회의 요약 결과를 Redis에 캐싱해 반복 조회 비용을 줄였다.', 1), + (5, NOW(), NOW(), 9, 'Flyway 마이그레이션과 mock 데이터 스크립트를 도입했다.', 0); + +-- meeting (종료된 회의 2건) +INSERT INTO meeting (meeting_id, created_at, updated_at, team_id, name, start_date_time, end_date_time, audio_key, audio_egress_id, is_normally_ended) VALUES + (1, NOW(), NOW(), 1, '주간 스프린트 회의', DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 1 HOUR, NULL, NULL, 1), + (2, NOW(), NOW(), 1, '결정 근거 회고 회의', DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 45 MINUTE, NULL, NULL, 1); + +INSERT INTO meeting_member (meeting_id, member_id, created_at, updated_at, role) VALUES + (1, 1, NOW(), NOW(), 'OWNER'), + (1, 2, NOW(), NOW(), 'GENERAL'), + (1, 3, NOW(), NOW(), 'GENERAL'), + (2, 1, NOW(), NOW(), 'OWNER'), + (2, 3, NOW(), NOW(), 'GENERAL'), + (2, 4, NOW(), NOW(), 'GENERAL'); + +INSERT INTO dialogue (dialogue_id, created_at, updated_at, meeting_id, member_id, content, speech_datetime) VALUES + (1, NOW(), NOW(), 1, 1, '오늘은 로그인 기능 마무리하고 회의 종료 버그를 같이 볼게요.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 1 MINUTE), + (2, NOW(), NOW(), 1, 2, '회의 종료 시간이 UTC로 저장돼서 어긋나는 것 같아요.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 6 MINUTE), + (3, NOW(), NOW(), 1, 1, 'JPA 설정에 타임존을 명시하지 않은 게 원인 같습니다.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 11 MINUTE), + (4, NOW(), NOW(), 1, 3, '결정 근거 조회 쿼리도 N+1이 나서 같이 정리하면 좋겠어요.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 16 MINUTE), + (5, NOW(), NOW(), 1, 2, '그럼 이번 스프린트 최우선 과제로 올릴게요.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 21 MINUTE), + (6, NOW(), NOW(), 1, 1, '로그인 API는 오늘 리뷰 요청 올리겠습니다.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 26 MINUTE), + (7, NOW(), NOW(), 1, 3, '좋아요, 정리되면 다음 회의 때 결과 공유할게요.', DATE_SUB(NOW(), INTERVAL 2 DAY) + INTERVAL 31 MINUTE), + (8, NOW(), NOW(), 2, 1, '지난 회의에서 결정한 커밋 추천 정확도를 다시 점검해봤어요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 1 MINUTE), + (9, NOW(), NOW(), 2, 4, '커밋 메시지만으로는 관련 없는 커밋도 추천되는 경우가 있더라고요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 7 MINUTE), + (10, NOW(), NOW(), 2, 3, '임베딩 기반으로 유사도를 계산하면 나아질 것 같아요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 13 MINUTE), + (11, NOW(), NOW(), 2, 1, '회의 요약도 매번 다시 계산돼서 느려요. 캐싱을 붙이면 좋겠어요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 19 MINUTE), + (12, NOW(), NOW(), 2, 4, '그럼 임베딩 파이프라인이랑 캐싱을 이번 주 안에 붙여볼게요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 25 MINUTE), + (13, NOW(), NOW(), 2, 3, '다음 회고 때 추천 정확도 변화도 같이 확인해봐요.', DATE_SUB(NOW(), INTERVAL 1 DAY) + INTERVAL 30 MINUTE); + +INSERT INTO meeting_analysis (meeting_analysis_id, created_at, updated_at, meeting_id, meeting_title, meeting_purpose, meeting_duration, analysis_content, topics, core_context, application_titles, application_reasons) VALUES + (1, NOW(), NOW(), 1, '주간 스프린트 회의', '스프린트 진행 상황 공유 및 이슈 논의', '60분', + '로그인 기능, 회의 종료 시간 버그, 결정 근거 조회 성능 문제를 논의했다.', + '["로그인 기능","회의 종료 버그","쿼리 성능"]', + '["로그인 API 검증 로직 추가","회의 종료 시간 타임존 문제","N+1 쿼리 정리"]', + '["로그인 기능 적용","회의 종료 시간 수정","조회 쿼리 리팩터링"]', + '["보안 요구사항 충족을 위해","타임존 버그로 인한 데이터 불일치 방지를 위해","조회 성능 개선을 위해"]'), + (2, NOW(), NOW(), 2, '결정 근거 회고 회의', '커밋 추천 정확도 회고 및 개선 방향 논의', '45분', + '커밋 추천 정확도 문제의 원인을 임베딩 부재로 진단하고, 임베딩 파이프라인과 회의 요약 캐싱 도입을 결정했다.', + '["추천 정확도","임베딩","캐싱"]', + '["키워드 매칭만으로는 관련 없는 커밋이 추천됨","임베딩 유사도 기반 추천 필요","회의 요약 재계산 비용 문제"]', + '["임베딩 파이프라인 적용","회의 요약 캐싱 적용"]', + '["추천 정확도 개선을 위해","반복 조회 비용 절감을 위해"]'); + +-- decision & applications +INSERT INTO decision (decision_id, created_at, updated_at, meeting_id, is_created, reliability_score) VALUES + (1, NOW(), NOW(), 1, 1, 85), + (2, NOW(), NOW(), 2, 1, 78); + +INSERT INTO decision_base (decision_base_pk, created_at, updated_at, decision_id, content) VALUES + (1, NOW(), NOW(), 1, '로그인 기능은 이번 스프린트 내 배포한다.'), + (2, NOW(), NOW(), 1, '회의 종료 시간 버그와 결정 근거 조회 성능 문제를 이번 스프린트 최우선으로 수정한다.'), + (3, NOW(), NOW(), 2, '커밋 추천 정확도를 높이기 위해 임베딩 파이프라인과 회의 요약 캐싱을 우선 적용한다.'); + +INSERT INTO decision_commits (decision_commits_pk, created_at, updated_at, decision_id, commit_id) VALUES + (1, NOW(), NOW(), 1, 1), + (2, NOW(), NOW(), 1, 2), + (3, NOW(), NOW(), 1, 3), + (4, NOW(), NOW(), 2, 4), + (5, NOW(), NOW(), 2, 5), + (6, NOW(), NOW(), 2, 7), + (7, NOW(), NOW(), 2, 8); + +INSERT INTO decision_timeline (decision_timeline_pk, created_at, updated_at, decision_id, timestamp, step, content, member_id, utterance) VALUES + (1, NOW(), NOW(), 1, '00:01:00', '문제 제기', '로그인 기능과 회의 종료 버그를 같이 보기로 했다.', 1, '오늘은 로그인 기능 마무리하고 회의 종료 버그를 같이 볼게요.'), + (2, NOW(), NOW(), 1, '00:06:00', '문제 제기', '회의 종료 시간이 어긋나는 문제를 공유했다.', 2, '회의 종료 시간이 UTC로 저장돼서 어긋나는 것 같아요.'), + (3, NOW(), NOW(), 1, '00:11:00', '원인 분석', '타임존 설정 누락이 원인으로 지목됐다.', 1, 'JPA 설정에 타임존을 명시하지 않은 게 원인 같습니다.'), + (4, NOW(), NOW(), 1, '00:16:00', '원인 분석', '결정 근거 조회 쿼리의 N+1 문제가 함께 제기됐다.', 3, '결정 근거 조회 쿼리도 N+1이 나서 같이 정리하면 좋겠어요.'), + (5, NOW(), NOW(), 1, '00:21:00', '결정', '이번 스프린트에서 최우선으로 수정하기로 했다.', 2, '그럼 이번 스프린트 최우선 과제로 올릴게요.'), + (6, NOW(), NOW(), 2, '00:01:00', '문제 제기', '커밋 추천 정확도를 다시 점검했다.', 1, '지난 회의에서 결정한 커밋 추천 정확도를 다시 점검해봤어요.'), + (7, NOW(), NOW(), 2, '00:07:00', '원인 분석', '키워드 매칭만으로는 관련 없는 커밋도 추천된다는 점이 지적됐다.', 4, '커밋 메시지만으로는 관련 없는 커밋도 추천되는 경우가 있더라고요.'), + (8, NOW(), NOW(), 2, '00:13:00', '대안 검토', '임베딩 기반 유사도 계산이 대안으로 제시됐다.', 3, '임베딩 기반으로 유사도를 계산하면 나아질 것 같아요.'), + (9, NOW(), NOW(), 2, '00:25:00', '결정', '임베딩 파이프라인과 캐싱을 이번 주 안에 적용하기로 했다.', 4, '그럼 임베딩 파이프라인이랑 캐싱을 이번 주 안에 붙여볼게요.'); + +INSERT INTO application (application_id, created_at, updated_at, decision_id, name) VALUES + (1, NOW(), NOW(), 1, '로그인 기능 배포'), + (2, NOW(), NOW(), 1, '회의 종료 시간 버그 수정'), + (3, NOW(), NOW(), 1, '결정 근거 조회 쿼리 리팩터링'), + (4, NOW(), NOW(), 2, '커밋 분석 임베딩 파이프라인 적용'), + (5, NOW(), NOW(), 2, '회의 요약 캐싱 적용'); + +INSERT INTO application_base (application_id, decision_base_pk, created_at, updated_at) VALUES + (1, 1, NOW(), NOW()), + (2, 2, NOW(), NOW()), + (3, 2, NOW(), NOW()), + (4, 3, NOW(), NOW()), + (5, 3, NOW(), NOW()); + +INSERT INTO application_commits (application_id, decision_commits_pk, created_at, updated_at, reason, confidence) VALUES + (1, 1, NOW(), NOW(), '로그인 API 구현 커밋이 결정 내용과 직접 연관된다.', 90), + (2, 2, NOW(), NOW(), '회의 종료 시간 버그 수정 커밋이 결정 내용과 직접 연관된다.', 88), + (3, 3, NOW(), NOW(), '결정 근거 조회 쿼리 리팩터링 커밋이 결정 내용과 직접 연관된다.', 84), + (4, 4, NOW(), NOW(), '임베딩 파이프라인 구현 커밋이 결정 내용과 직접 연관된다.', 92), + (5, 6, NOW(), NOW(), '회의 요약 캐싱 적용 커밋이 결정 내용과 직접 연관된다.', 86); + +INSERT INTO application_timeline (application_id, decision_timeline_pk, created_at, updated_at) VALUES + (1, 2, NOW(), NOW()), + (2, 3, NOW(), NOW()), + (3, 4, NOW(), NOW()), + (4, 7, NOW(), NOW()), + (5, 8, NOW(), NOW()); + +INSERT INTO commit_connection (application_id, commit_id, created_at, updated_at) VALUES + (1, 1, NOW(), NOW()), + (2, 2, NOW(), NOW()), + (3, 3, NOW(), NOW()), + (4, 4, NOW(), NOW()), + (5, 7, NOW(), NOW());