-
Notifications
You must be signed in to change notification settings - Fork 19
refactor: ci/cd 오류 수정 flyway 버전 #1448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b5438bb
4ce7209
cb13e2a
5f5b5e3
1a70f07
b324e0a
6d10fac
e875bf8
a4c18a7
543b5aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package net.causw.app.main.domain.notification.notification.entity; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneAchievementStatus; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.shared.entity.BaseEntity; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.Index; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Builder(access = AccessLevel.PROTECTED) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Table(name = "tb_post_like_milestone_achievement", uniqueConstraints = { | ||
| @UniqueConstraint(name = "uk_post_like_milestone_achievement_post_milestone", columnNames = {"post_id", | ||
| "milestone_count"}) | ||
| }, indexes = { | ||
| @Index(name = "idx_post_like_milestone_achievement_trigger_user", columnList = "trigger_user_id") | ||
| }) | ||
| public class PostLikeMilestoneAchievement extends BaseEntity { | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "post_id", nullable = false, updatable = false) | ||
| private Post post; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "trigger_user_id", updatable = false) | ||
| private User triggerUser; | ||
|
|
||
| @Column(name = "milestone_count", nullable = false, updatable = false) | ||
| private long milestoneCount; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "status", nullable = false, length = 32) | ||
| private PostLikeMilestoneAchievementStatus status; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "suppression_reason", length = 32) | ||
| private PostLikeMilestoneSuppressionReason suppressionReason; | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "notification_id", unique = true) | ||
| private Notification notification; | ||
|
|
||
| public static PostLikeMilestoneAchievement pending(Post post, User triggerUser, long milestoneCount) { | ||
| return PostLikeMilestoneAchievement.builder() | ||
| .post(post) | ||
| .triggerUser(triggerUser) | ||
| .milestoneCount(milestoneCount) | ||
| .status(PostLikeMilestoneAchievementStatus.PENDING) | ||
| .build(); | ||
| } | ||
|
|
||
| public static PostLikeMilestoneAchievement baselined(Post post, long milestoneCount) { | ||
| return PostLikeMilestoneAchievement.builder() | ||
| .post(post) | ||
| .milestoneCount(milestoneCount) | ||
| .status(PostLikeMilestoneAchievementStatus.BASELINED) | ||
| .build(); | ||
| } | ||
|
|
||
| public void markNotificationCreated(Notification notification) { | ||
| this.notification = notification; | ||
| this.suppressionReason = null; | ||
| this.status = PostLikeMilestoneAchievementStatus.NOTIFICATION_CREATED; | ||
| } | ||
|
|
||
| public void suppress(PostLikeMilestoneSuppressionReason suppressionReason) { | ||
| this.notification = null; | ||
| this.suppressionReason = suppressionReason; | ||
| this.status = PostLikeMilestoneAchievementStatus.SUPPRESSED; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package net.causw.app.main.domain.notification.notification.enums; | ||
|
|
||
| public enum PostLikeMilestoneAchievementStatus { | ||
| PENDING, | ||
| BASELINED, | ||
| NOTIFICATION_CREATED, | ||
| SUPPRESSED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package net.causw.app.main.domain.notification.notification.enums; | ||
|
|
||
| public enum PostLikeMilestoneSuppressionReason { | ||
| SELF_LIKE, | ||
| SETTING_DISABLED, | ||
| BLOCKED, | ||
| TARGET_UNAVAILABLE | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package net.causw.app.main.domain.notification.notification.event; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.service.dto.PushNotificationData; | ||
|
|
||
| public record PostLikeMilestonePushEvent( | ||
| String recipientUserId, | ||
| String pushTitle, | ||
| String pushBody, | ||
| PushNotificationData pushData) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package net.causw.app.main.domain.notification.notification.event; | ||
|
|
||
| public record PostLikeMilestoneReachedEvent(String postId, String likerId, long milestoneCount) { | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package net.causw.app.main.domain.notification.notification.repository; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
|
|
||
| @Repository | ||
| public interface PostLikeMilestoneAchievementRepository | ||
| extends JpaRepository<PostLikeMilestoneAchievement, String> { | ||
|
|
||
| boolean existsByPostIdAndMilestoneCount(String postId, long milestoneCount); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.repository.PostLikeMilestoneAchievementRepository; | ||
| import net.causw.app.main.shared.exception.errorcode.PostLikeMilestoneAchievementErrorCode; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class PostLikeMilestoneAchievementReader { | ||
|
|
||
| private final PostLikeMilestoneAchievementRepository achievementRepository; | ||
|
|
||
| public PostLikeMilestoneAchievement findById(String achievementId) { | ||
| return achievementRepository.findById(achievementId) | ||
| .orElseThrow( | ||
| PostLikeMilestoneAchievementErrorCode.POST_LIKE_MILESTONE_ACHIEVEMENT_NOT_FOUND::toBaseException); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.community.post.service.implementation.PostReader; | ||
| import net.causw.app.main.domain.notification.notification.event.PostLikeMilestoneReachedEvent; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.domain.user.account.service.implementation.UserReader; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class PostLikeMilestoneAchievementRecorder { | ||
|
|
||
| private final PostReader postReader; | ||
| private final UserReader userReader; | ||
| private final PostLikeMilestoneAchievementWriter achievementWriter; | ||
|
|
||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public Optional<String> record(PostLikeMilestoneReachedEvent event) { | ||
| Post post = postReader.findById(event.postId()); | ||
| User liker = userReader.findUserById(event.likerId()); | ||
|
|
||
| return achievementWriter.savePendingIfAbsent(post, liker, event.milestoneCount()) | ||
| .map(achievement -> achievement.getId()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.entity.Notification; | ||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.notification.notification.repository.PostLikeMilestoneAchievementRepository; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class PostLikeMilestoneAchievementWriter { | ||
|
Comment on lines
+17
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift 트랜잭션 경계를 Service 메서드로 이동하세요.
유스케이스를 조합하는 Service 메서드 또는 명시적인 유스케이스 진입점에 트랜잭션 경계를 두세요. Writer는 영속 상태 변경만 수행하게 유지하세요. As per path instructions, "트랜잭션 경계( 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| private final PostLikeMilestoneAchievementRepository achievementRepository; | ||
|
|
||
| public Optional<PostLikeMilestoneAchievement> savePendingIfAbsent( | ||
| Post post, | ||
| User triggerUser, | ||
| long milestoneCount) { | ||
| if (achievementRepository.existsByPostIdAndMilestoneCount(post.getId(), milestoneCount)) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| PostLikeMilestoneAchievement achievement = PostLikeMilestoneAchievement.pending( | ||
| post, | ||
| triggerUser, | ||
| milestoneCount); | ||
| return Optional.of(achievementRepository.saveAndFlush(achievement)); | ||
|
Comment on lines
+24
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline \
app-main/src/main/java/net/causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievement.java \
--items all
fd -t f -0 'V.*__.*\.sql$' . |
xargs -0 -r rg -n -C 4 \
'PostLikeMilestoneAchievement|post_id|milestone_count|UNIQUE|unique'
rg -n -C 5 \
'uniqueConstraints|post_id|milestoneCount|milestone_count' \
app-main/src/main/java/net/causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievement.javaRepository: CAUCSE/CAUSW_backend Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## Candidate files"
fd -t f \
'PostLikeMilestoneAchievementWriter.java|PostLikeMilestoneAchievementTest.java|PostLikeMilestoneAchievement.java|PostLikeMilestoneAchievementRepository.java|.*Notification.*Service.*\.java|.*Application.*Listener.*\.java|.*Event.*\.java' \
app-main/src/main/java app-main/src/test/java
echo
echo "## Writer"
cat -n app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementWriter.java
echo
echo "## Entity"
cat -n app-main/src/main/java/net/causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievement.java
echo
echo "## Repository"
fd -t f 'PostLikeMilestoneAchievementRepository.java' app-main/src/main/java -x cat -n {}
echo
echo "## Test"
cat -n app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementWriterTest.java
echo
echo "## Migrations mentioning entity/table names"
rg -n -C 4 'PostLikeMilestoneAchievement|post_like_milestone|milestone_count|milestone_count|trigger_user_id|status' app-main/src/main/resources/db/migration app-main/src/main/java/net/causw/app/main/domain/notification || true
echo
echo "## Call sites"
rg -n -C 4 'savePendingIfAbsent|PostLikeMilestoneAchievementWriter|PostLikeMilestoneAchievement' app-main/src/main/java app-main/src/test/javaRepository: CAUCSE/CAUSW_backend Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## PostLikeMilestoneAchievement call sites"
rg -n -C 5 'savePendingIfAbsent|PostLikeMilestoneAchievementWriter|PostLikeMilestoneReachedEvent|PostLikeMilestoneReachedListener|PostLikeMilestoneNotificationProcessor|transactionalEventListener' \
app-main/src/main/java/net/causw/app/main/domain/notification app-main/src/test/java
echo
echo "## Writer usages in repositories/services via direct grep across app-main"
rg -n -C 4 'PostLikeMilestoneAchievementWriter|savePendingIfAbsent' app-main/src/main/java app-main/src/test/java || trueRepository: CAUCSE/CAUSW_backend Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## Conflict handling symbols"
rg -n -C 3 'DataIntegrityViolationException|isPostMilestoneUniqueConstraintViolation|PostLikeMilestoneAchievementErrorCode|PostLikeMilestoneAchievement' \
app-main/src/main/java/net/causw/app/main/domain/notification | head -n 220
echo
echo "## Focused constraint check"
python3 - <<'PY'
from pathlib import Path
target = "tb_post_like_milestone_achievement"
for path in Path("app-main/src/main/resources/db/migration").glob("*.sql"):
txt = path.read_text(errors="ignore").lower()
if target in txt:
lines = txt.splitlines()
for i, line in enumerate(lines, 1):
if target in line or "uniqu" in line or "constraint" in line:
print(f"{path}:{i}:{line.strip()}")
PYRepository: CAUCSE/CAUSW_backend Length of output: 32447 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## ErrorCodes related to milestone achievement"
fd -t f 'PostLikeMilestoneAchievementErrorCode.java|.*ErrorCode.*java' app-main/src/main/java/net/causw/app/main/domain -x sh -c 'echo "--- $1"; rg -n -C 3 "POST_LIKE_MILESTONE_ACHIEVEMENT|milestone|Unique|UNIQUE|unique" "$1" || true' sh {}
echo
echo "## Base exception / runtime exception handling"
rg -n -C 3 'BaseException|BaseRunTimeV2Exception|RuntimeException|`@ExceptionHandler`|DataIntegrityViolationException|Postlike' app-main/src/main/java/net/causw/app/main/shared app-main/src/main/java/net/causw/app/main/domain/notification | head -n 260Repository: CAUCSE/CAUSW_backend Length of output: 27673 마일스톤 중복 방지 로직을 원자적으로 처리하세요.
현재 또한 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
|
|
||
| public PostLikeMilestoneAchievement suppress( | ||
| PostLikeMilestoneAchievement achievement, | ||
| PostLikeMilestoneSuppressionReason suppressionReason) { | ||
| achievement.suppress(suppressionReason); | ||
| return achievementRepository.save(achievement); | ||
| } | ||
|
|
||
| public PostLikeMilestoneAchievement markNotificationCreated( | ||
| PostLikeMilestoneAchievement achievement, | ||
| Notification notification) { | ||
| achievement.markNotificationCreated(notification); | ||
| return achievementRepository.save(achievement); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package net.causw.app.main.domain.notification.notification.service.implementation; | ||
|
|
||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import net.causw.app.main.domain.community.common.service.CommunityPermissionPolicy; | ||
| import net.causw.app.main.domain.community.post.entity.Post; | ||
| import net.causw.app.main.domain.notification.notification.entity.Notification; | ||
| import net.causw.app.main.domain.notification.notification.entity.PostLikeMilestoneAchievement; | ||
| import net.causw.app.main.domain.notification.notification.enums.NoticeType; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneAchievementStatus; | ||
| import net.causw.app.main.domain.notification.notification.enums.PostLikeMilestoneSuppressionReason; | ||
| import net.causw.app.main.domain.notification.notification.enums.UserNotificationSettingKey; | ||
| import net.causw.app.main.domain.notification.notification.event.PostLikeMilestonePushEvent; | ||
| import net.causw.app.main.domain.notification.notification.service.dto.PushNotificationData; | ||
| import net.causw.app.main.domain.notification.notification.service.dto.UserNotificationSettingMap; | ||
| import net.causw.app.main.domain.user.account.entity.user.User; | ||
| import net.causw.app.main.domain.user.relation.service.implementation.BlockReader; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class PostLikeMilestoneNotificationProcessor { | ||
|
|
||
| private final PostLikeMilestoneAchievementReader achievementReader; | ||
| private final PostLikeMilestoneAchievementWriter achievementWriter; | ||
| private final NotificationWriter notificationWriter; | ||
| private final NotificationSettingReader notificationSettingReader; | ||
| private final BlockReader blockReader; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
|
|
||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void process(String achievementId) { | ||
| PostLikeMilestoneAchievement achievement = achievementReader.findById(achievementId); | ||
| if (achievement.getStatus() != PostLikeMilestoneAchievementStatus.PENDING) { | ||
| return; | ||
| } | ||
|
|
||
| Post post = achievement.getPost(); | ||
| User liker = achievement.getTriggerUser(); | ||
| User postWriter = post == null ? null : post.getWriter(); | ||
|
|
||
| if (liker == null || isTargetUnavailable(post, postWriter)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.TARGET_UNAVAILABLE); | ||
| return; | ||
| } | ||
|
|
||
| if (liker.getId().equals(postWriter.getId())) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.SELF_LIKE); | ||
| return; | ||
| } | ||
|
|
||
| UserNotificationSettingMap settingMap = notificationSettingReader.findSettingMap(postWriter.getId()); | ||
| if (!settingMap.get(UserNotificationSettingKey.COMMUNITY_LIKE_ON_MY_POST)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.SETTING_DISABLED); | ||
| return; | ||
| } | ||
|
|
||
| if (blockReader.existsByBlockerAndBlocked(postWriter, liker)) { | ||
| achievementWriter.suppress(achievement, PostLikeMilestoneSuppressionReason.BLOCKED); | ||
| return; | ||
| } | ||
|
|
||
| long likeCount = achievement.getMilestoneCount(); | ||
| String serviceTitle = String.format("게시물이 좋아요 %d개를 달성했습니다!", likeCount); | ||
| String serviceBody = String.format("내 게시글에 좋아요가 %d개 달렸어요.", likeCount); | ||
| String pushTitle = String.format("게시물 좋아요 %d개 달성", likeCount); | ||
| PushNotificationData pushData = new PushNotificationData(NoticeType.COMMUNITY, post.getId(), | ||
| post.getBoard().getId()); | ||
|
|
||
| Notification notification = notificationWriter.save( | ||
| Notification.of(postWriter, serviceTitle, serviceBody, NoticeType.COMMUNITY, post.getId(), | ||
| post.getBoard().getId())); | ||
| notificationWriter.saveLog(postWriter, notification); | ||
| achievementWriter.markNotificationCreated(achievement, notification); | ||
|
|
||
| eventPublisher.publishEvent(new PostLikeMilestonePushEvent( | ||
| postWriter.getId(), | ||
| pushTitle, | ||
| serviceBody, | ||
| pushData)); | ||
| } | ||
|
|
||
| private boolean isTargetUnavailable(Post post, User postWriter) { | ||
| return !CommunityPermissionPolicy.isAlive(post) | ||
| || postWriter == null | ||
| || postWriter.isInactive() | ||
| || postWriter.isDropped(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: CAUCSE/CAUSW_backend
Length of output: 8299
Flyway 스키마 변경을 함께 추가해 주세요.
tb_post_like_milestone_achievement테이블, 외래 키(post_id,trigger_user_id,notification_id),uk_post_like_milestone_achievement_post_milestone,idx_post_like_milestone_achievement_trigger_user변경이 엔티티에 추가되어 있으므로, 이 PR에VYYYYMMDDHHMMSS__create_post_like_milestone_achievement.sqlFlyway 마이그레이션과db-change라벨을 포함해야 합니다.🤖 Prompt for AI Agents
Source: Path instructions