Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ public class BirdIdRequestHistoryRecorder {

/** 컬렉션 생성 직후, bird가 비어있고 PUBLIC인 경우 pending 시작을 기록 */
public void onCollectionCreatedIfPending(UserBirdCollection collection, OffsetDateTime startedAt) {
if (collection.getBird() != null) return;
if (collection.getAccessLevel() != PUBLIC) return;
if (!collection.canReceiveBirdIdSuggestions()) return;
if (repo.findOpenByCollectionId(collection.getId()).isPresent()) return;
repo.save(BirdIdRequestHistory.start(collection, startedAt));
}

/** 현재 컬렉션 상태를 기준으로 열린 동정 요청 이력을 생성하거나 취소 */
public void syncOpenState(UserBirdCollection collection, OffsetDateTime now) {
if (collection.canReceiveBirdIdSuggestions()) {
if (repo.findOpenByCollectionId(collection.getId()).isEmpty()) {
repo.save(BirdIdRequestHistory.start(collection, now));
}
return;
}
repo.deleteOpenByCollectionId(collection.getId());
}

/** 채택(ADOPT)으로 해결된 순간 */
public void onResolvedByAdopt(UserBirdCollection collection, OffsetDateTime resolvedAt) {
repo.findOpenByCollectionId(collection.getId())
Expand All @@ -42,7 +52,7 @@ public void onResolvedByEdit(UserBirdCollection collection) {

/** not null -> null 로 바뀌는 순간: PUBLIC이면 새 pending 시작 */
public void onBirdSetToUnknown(UserBirdCollection collection, OffsetDateTime startedAt) {
if (collection.getAccessLevel() != PUBLIC) return;
if (!collection.canReceiveBirdIdSuggestions()) return;
if (repo.findOpenByCollectionId(collection.getId()).isPresent()) return;
repo.save(BirdIdRequestHistory.start(collection, startedAt));
}
Expand All @@ -55,7 +65,7 @@ public void onAccessLevelChanged(UserBirdCollection collection, AccessLevelType
repo.deleteOpenByCollectionId(collection.getId());
} else if (oldLevel == PRIVATE && newLevel == PUBLIC) {
// 공개로 바뀌었고 아직 미식별이면 새로 오픈
if (collection.getBird() == null && repo.findOpenByCollectionId(collection.getId()).isEmpty()) {
if (collection.canReceiveBirdIdSuggestions() && repo.findOpenByCollectionId(collection.getId()).isEmpty()) {
repo.save(BirdIdRequestHistory.start(collection, now));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ public class CreateCollectionRequest {

@Schema(description = "공개/비공개 여부", example = "PUBLIC", nullable = true)
private AccessLevelType accessLevel;

@Schema(description = "birdId가 null일 때 동정 의견을 받을지 여부. 생략 시 true", example = "true", nullable = true)
private Boolean birdIdSuggestionEnabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ public class UpdateCollectionRequest {

@Schema(description = "공개/비공개 여부", example = "PUBLIC", nullable = true)
private AccessLevelType accessLevel;

@Schema(description = "birdId가 null일 때 동정 의견을 받을지 여부. null이면 변경하지 않음", example = "false", nullable = true)
private Boolean birdIdSuggestionEnabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public class GetCollectionDetailResponse {
@Schema(description = "내 컬렉션인지 여부", example = "false")
private Boolean isMine;

@Schema(description = "동정 의견을 받을 수 있는 상태인지 여부", example = "true")
private Boolean canSuggestBirdId;

@Schema(description = "새 정보")
private BirdInfo bird;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class GetCollectionEditDataResponse {
@Schema(description = "컬렉션 공개 범위 (공개/비공개)")
private AccessLevelType accessLevel;

@Schema(description = "birdId가 null일 때 동정 의견을 받을지 여부", example = "true")
private Boolean birdIdSuggestionEnabled;

@Schema(description = "이미지 ID", example = "300")
private Long imageId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ public record UpdateCollectionResponse(
String imageUrl,

@Schema(description = "공개/비공개 여부", example = "PUBLIC", nullable = true)
AccessLevelType accessLevel
AccessLevelType accessLevel,

@Schema(description = "birdId가 null일 때 동정 의견을 받을지 여부", example = "true")
Boolean birdIdSuggestionEnabled
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId
if (collection.getBird() != null)
throw new BadRequestException("이미 bird_id가 확정된 컬렉션이에요");

if (!collection.canReceiveBirdIdSuggestions())
throw new BadRequestException("동정 의견을 받지 않는 컬렉션이에요");

if (collection.getUser().getId().equals(userId))
throw new BadRequestException("나 자신의 컬렉션에 동정 의견을 제안할 수 없어요");

Expand Down Expand Up @@ -106,6 +109,9 @@ public ToggleStatusResponse toggleAgree(Long userId, Long collectionId, Long bir
if (collection.getBird() != null)
throw new BadRequestException("이미 bird_id가 확정된 컬렉션이에요");

if (!collection.canReceiveBirdIdSuggestions())
throw new BadRequestException("동정 의견을 받지 않는 컬렉션이에요");

if (collection.getUser().getId().equals(userId))
throw new BadRequestException("나 자신의 컬렉션에 동의할 수 없어요");

Expand Down Expand Up @@ -156,6 +162,9 @@ public ToggleStatusResponse toggleDisagree(Long userId, Long collectionId, Long
if (collection.getBird() != null)
throw new BadRequestException("이미 bird_id가 확정된 컬렉션이에요");

if (!collection.canReceiveBirdIdSuggestions())
throw new BadRequestException("동정 의견을 받지 않는 컬렉션이에요");

if (collection.getUser().getId().equals(userId))
throw new BadRequestException("나 자신의 컬렉션에 비동의할 수 없어요");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.devkor.apu.saerok_server.domain.collection.application.dto.CreateCollectionCommand;
import org.devkor.apu.saerok_server.domain.collection.application.dto.DeleteCollectionCommand;
import org.devkor.apu.saerok_server.domain.collection.application.dto.UpdateCollectionCommand;
import org.devkor.apu.saerok_server.domain.collection.core.entity.AccessLevelType;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionImageRepository;
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
Expand Down Expand Up @@ -60,6 +59,7 @@ public Long createCollection(CreateCollectionCommand command) {
throw new BadRequestException("한 줄 평 길이는 " + UserBirdCollection.NOTE_MAX_LENGTH + "자 이하여야 해요");

Point location = PointFactory.create(command.latitude(), command.longitude());
boolean birdIdSuggestionEnabled = bird == null && !Boolean.FALSE.equals(command.birdIdSuggestionEnabled());

UserBirdCollection collection = UserBirdCollection.builder()
.user(user)
Expand All @@ -71,12 +71,12 @@ public Long createCollection(CreateCollectionCommand command) {
.address(command.address())
.note(command.note())
.accessLevel(command.accessLevel())
.birdIdSuggestionEnabled(birdIdSuggestionEnabled)
.build();

Long id = collectionRepository.save(collection);

// 생성 직후 bird가 비어 있고 PUBLIC이면 '대기 시작' 기록
birdReqHistory.onCollectionCreatedIfPending(collection, collection.getCreatedAt());
birdReqHistory.syncOpenState(collection, collection.getCreatedAt());

return id;
}
Expand Down Expand Up @@ -110,25 +110,19 @@ public UpdateCollectionResponse updateCollection(UpdateCollectionCommand command
}

OffsetDateTime now = OffsetDateTime.now();
// 변경 전 상태 스냅샷
AccessLevelType oldLevel = collection.getAccessLevel();

// 새 ID 변경
if (Boolean.TRUE.equals(command.isBirdIdUpdated())) {
Bird before = collection.getBird();
Bird after = (command.birdId() != null)
? birdRepository.findById(command.birdId()).orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"))
: null;

if (before == null && after != null) {
// null -> not null : EDIT로 해결 → 열린 기록 삭제
birdReqHistory.onResolvedByEdit(collection);
} else if (before != null && after == null) {
// not null -> null : PUBLIC이면 다시 대기 시작
birdReqHistory.onBirdSetToUnknown(collection, now);
}
boolean becameUnknown = before != null && after == null;

collection.changeBird(after);
if (becameUnknown && command.birdIdSuggestionEnabled() == null) {
collection.changeBirdIdSuggestionEnabled(true);
}
}

if (command.discoveredDate() != null) collection.setDiscoveredDate(command.discoveredDate());
Expand All @@ -150,12 +144,16 @@ public UpdateCollectionResponse updateCollection(UpdateCollectionCommand command
collection.setNote(command.note());
}

// 액세스 레벨 변경 처리 (전/후 비교)
if (command.accessLevel() != null) {
collection.setAccessLevel(command.accessLevel());
birdReqHistory.onAccessLevelChanged(collection, oldLevel, now);
}

if (command.birdIdSuggestionEnabled() != null) {
collection.changeBirdIdSuggestionEnabled(command.birdIdSuggestionEnabled());
}

birdReqHistory.syncOpenState(collection, now);

String imageUrl = collectionImageRepository.findObjectKeysByCollectionId(command.collectionId()).stream()
.map(imageDomainService::toUploadImageUrl)
.findFirst()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public record CreateCollectionCommand (
String locationAlias,
String address,
String note,
AccessLevelType accessLevel
AccessLevelType accessLevel,
Boolean birdIdSuggestionEnabled
){
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public record UpdateCollectionCommand (
String locationAlias,
String address,
String note,
AccessLevelType accessLevel
AccessLevelType accessLevel,
Boolean birdIdSuggestionEnabled
){
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ public class UserBirdCollection extends Auditable {
@Setter
private AccessLevelType accessLevel;

@Column(name = "bird_id_suggestion_enabled", nullable = false)
private boolean birdIdSuggestionEnabled = true;

@Builder
public UserBirdCollection(User user, Bird bird, String tempBirdName, LocalDate discoveredDate, Point location, String locationAlias, String address, String note, boolean isPinned, AccessLevelType accessLevel) {
public UserBirdCollection(User user, Bird bird, String tempBirdName, LocalDate discoveredDate, Point location, String locationAlias, String address, String note, boolean isPinned, AccessLevelType accessLevel, Boolean birdIdSuggestionEnabled) {

if (user == null) throw new IllegalArgumentException("user는 null일 수 없습니다.");
if (discoveredDate == null) throw new IllegalArgumentException("discoveredDate는 null일 수 없습니다.");
Expand All @@ -79,11 +82,23 @@ public UserBirdCollection(User user, Bird bird, String tempBirdName, LocalDate d
this.note = note;
this.isPinned = isPinned;
this.accessLevel = accessLevel == null ? AccessLevelType.PUBLIC : accessLevel;
this.birdIdSuggestionEnabled = bird == null && (birdIdSuggestionEnabled == null || birdIdSuggestionEnabled);
}

/** 단순 변경: 동정 요청 기록 열고/닫기는 별도 Recorder가 처리 */
public void changeBird(Bird newBird) {
this.bird = newBird;
if (newBird != null) {
this.birdIdSuggestionEnabled = false;
}
}

public void changeBirdIdSuggestionEnabled(boolean enabled) {
this.birdIdSuggestionEnabled = getBird() == null && enabled;
}

public boolean canReceiveBirdIdSuggestions() {
return bird == null && accessLevel == AccessLevelType.PUBLIC && birdIdSuggestionEnabled;
}

public double getLongitude() { return location.getX(); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ public List<UserBirdCollection> findPublicPendingCollections() {
SELECT c FROM UserBirdCollection c
JOIN FETCH c.user u
JOIN BirdIdRequestHistory h ON h.collection.id = c.id AND h.resolvedAt IS NULL
WHERE c.accessLevel = :public AND c.bird IS NULL
WHERE c.accessLevel = :public
AND c.bird IS NULL
AND c.birdIdSuggestionEnabled = true
ORDER BY h.startedAt DESC
""", UserBirdCollection.class)
.setParameter("public", AccessLevelType.PUBLIC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public interface CollectionWebMapper {
@Mapping(target = "commentCount", source = "commentCount")
@Mapping(target = "isLiked", source = "isLiked")
@Mapping(target = "isMine", source = "isMine")
@Mapping(target = "canSuggestBirdId", expression = "java(collection.canReceiveBirdIdSuggestions())")
@Mapping(target = "latitude", expression = "java(CollectionLocationMasker.latitude(collection, isMine))")
@Mapping(target = "longitude", expression = "java(CollectionLocationMasker.longitude(collection, isMine))")
@Mapping(target = "locationAlias", expression = "java(CollectionLocationMasker.locationAlias(collection, isMine))")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public record CommunityCollectionInfo(

@Schema(description = "동정 돕기에 참여한 유저 수 (동정 요청 컬렉션인 경우에만)", example = "5", nullable = true)
Long suggestionUserCount,

@Schema(description = "동정 의견을 받을 수 있는 상태인지 여부", example = "true")
Boolean canSuggestBirdId,

@Schema(description = "새 정보")
BirdInfo bird,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public List<CommunityCollectionInfo> toCollectionInfos(List<UserBirdCollection>
Map<Long, Boolean> popularStatusMap = popularCollectionRepository.existsByCollectionIds(collectionIds);

List<Long> pendingCollectionIds = collections.stream()
.filter(c -> c.getBird() == null)
.filter(UserBirdCollection::canReceiveBirdIdSuggestions)
.map(UserBirdCollection::getId)
.toList();
Map<Long, Long> suggestionUserCounts = pendingCollectionIds.isEmpty()
Expand All @@ -62,7 +62,7 @@ public List<CommunityCollectionInfo> toCollectionInfos(List<UserBirdCollection>
boolean isPopular = popularStatusMap.getOrDefault(collection.getId(), false);
boolean isMine = userId != null && userId.equals(collection.getUser().getId());

Long suggestionUserCount = collection.getBird() == null
Long suggestionUserCount = collection.canReceiveBirdIdSuggestions()
? suggestionUserCounts.getOrDefault(collection.getId(), 0L)
: null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public List<UserBirdCollection> findPendingBirdIdCollections(CommunityQueryComma
SELECT c FROM UserBirdCollection c
JOIN FETCH c.user u
JOIN BirdIdRequestHistory h ON h.collection.id = c.id AND h.resolvedAt IS NULL
WHERE c.accessLevel = :public AND c.bird IS NULL
WHERE c.accessLevel = :public
AND c.bird IS NULL
AND c.birdIdSuggestionEnabled = true
ORDER BY h.startedAt DESC
""", UserBirdCollection.class)
.setParameter("public", AccessLevelType.PUBLIC);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public interface CommunityWebMapper {
@Mapping(target = "isLiked", source = "isLiked")
@Mapping(target = "isPopular", source = "isPopular")
@Mapping(target = "suggestionUserCount", source = "suggestionUserCount")
@Mapping(target = "canSuggestBirdId", expression = "java(collection.canReceiveBirdIdSuggestions())")
@Mapping(target = "bird", expression = "java(mapBirdInfo(collection))")
@Mapping(target = "user", expression = "java(mapUserInfo(collection, userProfileImageUrl, thumbnailProfileImageUrl))")
CommunityCollectionInfo toCommunityCollectionInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE user_bird_collection
ADD COLUMN bird_id_suggestion_enabled BOOLEAN NOT NULL DEFAULT TRUE;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.devkor.apu.saerok_server.domain.collection.application;

import org.devkor.apu.saerok_server.domain.collection.api.dto.response.SuggestBirdIdResponse;
import org.devkor.apu.saerok_server.domain.collection.core.entity.AccessLevelType;
import org.devkor.apu.saerok_server.domain.collection.core.entity.BirdIdSuggestion;
import org.devkor.apu.saerok_server.domain.collection.core.entity.BirdIdSuggestion.SuggestionType;
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
Expand Down Expand Up @@ -58,6 +59,7 @@ private UserBirdCollection collection(long id, User owner) {
UserBirdCollection c = new UserBirdCollection();
ReflectionTestUtils.setField(c, "id", id);
ReflectionTestUtils.setField(c, "user", owner);
c.setAccessLevel(AccessLevelType.PUBLIC);
return c;
}

Expand Down Expand Up @@ -144,6 +146,22 @@ void userNotFound() {
assertThatThrownBy(() -> sut.suggest(1L, 100L, 5L)).isInstanceOf(org.devkor.apu.saerok_server.global.shared.exception.NotFoundException.class);
}

@Test @DisplayName("동정요청 비활성 컬렉션이면 제안 불가")
void suggestionDisabled() {
User u = user(1L);
UserBirdCollection col = collection(100L, user(2L));
col.changeBirdIdSuggestionEnabled(false);

when(userRepo.findById(1L)).thenReturn(Optional.of(u));
when(collectionRepo.findById(100L)).thenReturn(Optional.of(col));

assertThatThrownBy(() -> sut.suggest(1L, 100L, 5L))
.isInstanceOf(org.devkor.apu.saerok_server.global.shared.exception.BadRequestException.class)
.hasMessage("동정 의견을 받지 않는 컬렉션이에요");

verifyNoInteractions(birdRepo);
}

// … 이하 생략 (원본과 동일)
}
}
Loading
Loading