Skip to content

Commit 031ab5d

Browse files
Merge pull request #230 from DevKor-github/develop
#212 ~ #229까지 반영해 운영 서버 배포
2 parents c4bc14b + 4e9d0c5 commit 031ab5d

54 files changed

Lines changed: 1557 additions & 408 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
.DS_Store
3839

3940
# 환경 변수 파일은 절대 커밋하지 말 것!
4041
.env

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/CollectionController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ public GetCollectionDetailResponse getCollectionDetail(
240240
- koreanName
241241
- likeCount
242242
- commentCount
243-
243+
- createdAt
244+
- discoveredDate
244245
""",
245246
responses = {
246247
@ApiResponse(

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/dto/response/GetNearbyCollectionsResponse.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public static class Item {
2323
@Schema(description = "이미지 URL", example = "https://cdn.example.com/collection-images/1.jpg")
2424
private String imageUrl;
2525

26+
@Schema(description = "썸네일 이미지 URL (320px 너비)", example = "https://cdn.example.com/thumbnails/abc.webp")
27+
private String thumbnailImageUrl;
28+
2629
@Schema(description = "새 한국어 이름", example = "까치")
2730
private String koreanName;
2831

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/dto/response/MyCollectionsResponse.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import io.swagger.v3.oas.annotations.media.Schema;
44

5+
import java.time.LocalDate;
6+
import java.time.LocalDateTime;
57
import java.util.List;
68

79
@Schema(description = "내 컬렉션 목록 조회 응답")
@@ -17,13 +19,22 @@ public record Item(
1719
@Schema(description = "이미지 URL", example = "https://example.com/images/collection1.jpg")
1820
String imageUrl,
1921

22+
@Schema(description = "썸네일 이미지 URL (320px 너비)", example = "https://cdn.example.com/thumbnails/abc.webp")
23+
String thumbnailImageUrl,
24+
2025
@Schema(description = "새의 한국 이름", example = "까치")
2126
String koreanName,
2227

2328
@Schema(description = "좋아요 수", example = "15")
2429
Long likeCount,
2530

2631
@Schema(description = "댓글 수", example = "7")
27-
Long commentCount
32+
Long commentCount,
33+
34+
@Schema(description = "생성 날짜", example = "2024-03-15T10:30:00")
35+
LocalDateTime createdAt,
36+
37+
@Schema(description = "새 발견 일시", example = "2025-01-15")
38+
LocalDate discoveredDate
2839
) { }
2940
}

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/BirdIdSuggestionCommandService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
1212
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
1313
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
14+
import org.devkor.apu.saerok_server.domain.stat.application.BirdIdRequestHistoryRecorder;
1415
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1516
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1617
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
1718
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
1819
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
1920
import org.springframework.stereotype.Service;
2021

22+
import java.time.OffsetDateTime;
23+
2124
@Service
2225
@Transactional
2326
@RequiredArgsConstructor
@@ -28,6 +31,7 @@ public class BirdIdSuggestionCommandService {
2831
private final BirdRepository birdRepo;
2932
private final UserRepository userRepo;
3033
private final NotifyActionDsl notifyAction;
34+
private final BirdIdRequestHistoryRecorder birdReqHistory;
3135

3236
public SuggestBirdIdResponse suggest(Long userId, Long collectionId, Long birdId) {
3337
User user = userRepo.findById(userId)
@@ -210,6 +214,9 @@ public AdoptSuggestionResponse adopt(Long userId, Long collectionId, Long birdId
210214
Bird bird = birdRepo.findById(birdId)
211215
.orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"));
212216

217+
// 채택(ADOPT) 경로로 해결 기록
218+
birdReqHistory.onResolvedByAdopt(collection, OffsetDateTime.now());
219+
213220
collection.changeBird(bird);
214221

215222
// NOTE: 컬렉션에 달린 동정 의견들은 삭제되지 않음

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/BirdIdSuggestionQueryService.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1010
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1111
import org.devkor.apu.saerok_server.domain.user.core.service.UserProfileImageUrlService;
12+
import org.devkor.apu.saerok_server.domain.stat.core.repository.BirdIdRequestHistoryRepository; // ★ 추가
1213
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
1314
import org.devkor.apu.saerok_server.global.shared.infra.ImageDomainService;
1415
import org.devkor.apu.saerok_server.global.shared.util.OffsetDateTimeLocalizer;
1516
import org.springframework.stereotype.Service;
1617
import org.springframework.transaction.annotation.Transactional;
1718

19+
import java.time.OffsetDateTime; // ★ 추가
1820
import java.util.List;
1921
import java.util.Map;
2022

@@ -26,9 +28,10 @@ public class BirdIdSuggestionQueryService {
2628
private final BirdIdSuggestionRepository suggestionRepo;
2729
private final CollectionRepository collectionRepo;
2830
private final ImageDomainService imageDomainService;
29-
private final UserRepository userRepo;
31+
private final UserRepository userRepo;
3032
private final UserProfileImageUrlService userProfileImageUrlService;
31-
private final CollectionImageUrlService collectionImageUrlService;
33+
private final CollectionImageUrlService collectionImageUrlService;
34+
private final BirdIdRequestHistoryRepository birdIdRequestHistoryRepository; // ★ 추가
3235

3336
/* 전체 PUBLIC + pending 컬렉션 조회 */
3437
public GetPendingCollectionsResponse getPendingCollections() {
@@ -47,18 +50,27 @@ public GetPendingCollectionsResponse getPendingCollections() {
4750
.toList();
4851
Map<Long, String> profileImageUrls = userProfileImageUrlService.getProfileImageUrlsFor(users);
4952

53+
// ── 3.5단계: 열린 동정 요청 히스토리 startedAt 일괄 조회 (기존 c.getBirdIdSuggestionRequestedAt() 대체)
54+
List<Long> ids = collections.stream().map(UserBirdCollection::getId).toList();
55+
Map<Long, OffsetDateTime> startedAtMap = birdIdRequestHistoryRepository.findOpenStartedAtMapByCollectionIds(ids);
56+
5057
// ── 4단계: DTO 조립
5158
List<GetPendingCollectionsResponse.Item> items = collections.stream()
52-
.map(c -> new GetPendingCollectionsResponse.Item(
53-
c.getId(),
54-
thumbMap.getOrDefault(c.getId(), null) == null
55-
? null
56-
: imageDomainService.toUploadImageUrl(thumbMap.get(c.getId())),
57-
c.getNote(),
58-
c.getUser().getNickname(),
59-
profileImageUrls.get(c.getUser().getId()),
60-
OffsetDateTimeLocalizer.toSeoulLocalDateTime(c.getBirdIdSuggestionRequestedAt())
61-
))
59+
.map(c -> {
60+
OffsetDateTime startedAt = startedAtMap.get(c.getId()); // ★ 변경: 히스토리 기준
61+
return new GetPendingCollectionsResponse.Item(
62+
c.getId(),
63+
thumbMap.getOrDefault(c.getId(), null) == null
64+
? null
65+
: imageDomainService.toUploadImageUrl(thumbMap.get(c.getId())),
66+
c.getNote(),
67+
c.getUser().getNickname(),
68+
profileImageUrls.get(c.getUser().getId()),
69+
startedAt != null
70+
? OffsetDateTimeLocalizer.toSeoulLocalDateTime(startedAt)
71+
: null
72+
);
73+
})
6274
.toList();
6375

6476
return new GetPendingCollectionsResponse(items);

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionCommandService.java

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@
66
import org.devkor.apu.saerok_server.domain.collection.application.dto.CreateCollectionCommand;
77
import org.devkor.apu.saerok_server.domain.collection.application.dto.DeleteCollectionCommand;
88
import org.devkor.apu.saerok_server.domain.collection.application.dto.UpdateCollectionCommand;
9+
import org.devkor.apu.saerok_server.domain.collection.core.entity.AccessLevelType;
910
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
1011
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionImageRepository;
1112
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
1213
import org.devkor.apu.saerok_server.domain.collection.core.util.PointFactory;
1314
import org.devkor.apu.saerok_server.domain.collection.mapper.CollectionWebMapper;
1415
import org.devkor.apu.saerok_server.domain.dex.bird.core.entity.Bird;
1516
import org.devkor.apu.saerok_server.domain.dex.bird.core.repository.BirdRepository;
17+
import org.devkor.apu.saerok_server.domain.stat.application.BirdIdRequestHistoryRecorder;
1618
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
1719
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
1820
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
1921
import org.devkor.apu.saerok_server.global.shared.exception.ForbiddenException;
2022
import org.devkor.apu.saerok_server.global.shared.exception.NotFoundException;
2123
import org.devkor.apu.saerok_server.global.shared.infra.ImageDomainService;
2224
import org.devkor.apu.saerok_server.global.shared.infra.ImageService;
23-
import org.locationtech.jts.geom.Coordinate;
24-
import org.locationtech.jts.geom.GeometryFactory;
2525
import org.locationtech.jts.geom.Point;
26-
import org.locationtech.jts.geom.PrecisionModel;
2726
import org.springframework.stereotype.Service;
2827

28+
import java.time.OffsetDateTime;
2929
import java.util.List;
3030

3131
import static org.devkor.apu.saerok_server.global.shared.util.TransactionUtils.runAfterCommitOrNow;
@@ -42,29 +42,19 @@ public class CollectionCommandService {
4242
private final ImageDomainService imageDomainService;
4343
private final CollectionWebMapper collectionWebMapper;
4444
private final ImageService imageService;
45+
private final BirdIdRequestHistoryRecorder birdReqHistory;
4546

4647
public Long createCollection(CreateCollectionCommand command) {
4748
User user = userRepository.findById(command.userId()).orElseThrow(() -> new NotFoundException("존재하지 않는 사용자 id예요"));
4849

49-
Bird bird;
50+
Bird bird = (command.birdId() != null)
51+
? birdRepository.findById(command.birdId()).orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"))
52+
: null;
5053

51-
if (command.birdId() != null) {
52-
bird = birdRepository.findById(command.birdId()).orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"));
53-
} else {
54-
bird = null;
55-
}
56-
57-
if (command.discoveredDate() == null) {
58-
throw new BadRequestException("관찰 날짜를 포함해주세요");
59-
}
60-
61-
if (command.longitude() == null || command.latitude() == null) {
62-
throw new BadRequestException("관찰 위치 정보를 포함해주세요");
63-
}
64-
65-
if (command.note() != null && command.note().length() > UserBirdCollection.NOTE_MAX_LENGTH) {
54+
if (command.discoveredDate() == null) throw new BadRequestException("관찰 날짜를 포함해주세요");
55+
if (command.longitude() == null || command.latitude() == null) throw new BadRequestException("관찰 위치 정보를 포함해주세요");
56+
if (command.note() != null && command.note().length() > UserBirdCollection.NOTE_MAX_LENGTH)
6657
throw new BadRequestException("한 줄 평 길이는 " + UserBirdCollection.NOTE_MAX_LENGTH + "자 이하여야 해요");
67-
}
6858

6959
Point location = PointFactory.create(command.latitude(), command.longitude());
7060

@@ -80,7 +70,12 @@ public Long createCollection(CreateCollectionCommand command) {
8070
.accessLevel(command.accessLevel())
8171
.build();
8272

83-
return collectionRepository.save(collection);
73+
Long id = collectionRepository.save(collection);
74+
75+
// 생성 직후 bird가 비어 있고 PUBLIC이면 '대기 시작' 기록
76+
birdReqHistory.onCollectionCreatedIfPending(collection, collection.getCreatedAt());
77+
78+
return id;
8479
}
8580

8681
public void deleteCollection(DeleteCollectionCommand command) {
@@ -90,11 +85,15 @@ public void deleteCollection(DeleteCollectionCommand command) {
9085
throw new ForbiddenException("해당 컬렉션에 대한 권한이 없어요");
9186
}
9287

93-
List<String> objectKeys = collectionImageRepository.findObjectKeysByCollectionId(command.collectionId());
88+
// 1) 열린 동정 요청 히스토리 사전 정리
89+
birdReqHistory.onCollectionDeleted(collection.getId());
9490

91+
// 2) 연관 이미지 정리 후 컬렉션 삭제
92+
List<String> objectKeys = collectionImageRepository.findObjectKeysByCollectionId(command.collectionId());
9593
collectionImageRepository.removeByCollectionId(command.collectionId());
9694
collectionRepository.remove(collection);
9795
runAfterCommitOrNow(() -> imageService.deleteAll(objectKeys));
96+
// 3) 닫힌 히스토리는 FK가 SET NULL로 남는다
9897
}
9998

10099
public UpdateCollectionResponse updateCollection(UpdateCollectionCommand command) {
@@ -104,41 +103,51 @@ public UpdateCollectionResponse updateCollection(UpdateCollectionCommand command
104103
throw new ForbiddenException("해당 컬렉션에 대한 권한이 없어요");
105104
}
106105

107-
System.out.println(command.isBirdIdUpdated());
108-
if (command.isBirdIdUpdated() != null && command.isBirdIdUpdated()) {
109-
if (command.birdId() != null) {
110-
Bird bird = birdRepository.findById(command.birdId()).orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"));
111-
collection.changeBird(bird);
112-
} else {
113-
collection.changeBird(null);
106+
OffsetDateTime now = OffsetDateTime.now();
107+
// 변경 전 상태 스냅샷
108+
AccessLevelType oldLevel = collection.getAccessLevel();
109+
110+
// 새 ID 변경
111+
if (Boolean.TRUE.equals(command.isBirdIdUpdated())) {
112+
Bird before = collection.getBird();
113+
Bird after = (command.birdId() != null)
114+
? birdRepository.findById(command.birdId()).orElseThrow(() -> new NotFoundException("존재하지 않는 조류 id예요"))
115+
: null;
116+
117+
if (before == null && after != null) {
118+
// null -> not null : EDIT로 해결 → 열린 기록 삭제
119+
birdReqHistory.onResolvedByEdit(collection);
120+
} else if (before != null && after == null) {
121+
// not null -> null : PUBLIC이면 다시 대기 시작
122+
birdReqHistory.onBirdSetToUnknown(collection, now);
114123
}
124+
125+
collection.changeBird(after);
115126
}
116127

117128
if (command.discoveredDate() != null) collection.setDiscoveredDate(command.discoveredDate());
118129

119130
if ((command.latitude() == null) ^ (command.longitude() == null)) {
120131
throw new BadRequestException("위도와 경도 둘 중 하나만 수정할 수는 없어요");
121132
} else if (command.latitude() != null) {
122-
GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
123-
Coordinate coordinate = new Coordinate(command.longitude(), command.latitude());
124-
Point location = geometryFactory.createPoint(coordinate);
133+
var location = PointFactory.create(command.latitude(), command.longitude());
125134
collection.setLocation(location);
126135
}
127136

128137
if (command.locationAlias() != null) collection.setLocationAlias(command.locationAlias());
129-
130138
if (command.address() != null) collection.setAddress(command.address());
131139

132140
if (command.note() != null) {
133141
if (command.note().length() > UserBirdCollection.NOTE_MAX_LENGTH) {
134142
throw new BadRequestException("한 줄 평 길이는 " + UserBirdCollection.NOTE_MAX_LENGTH + "자 이하여야 해요");
135143
}
136-
137144
collection.setNote(command.note());
138145
}
139146

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

144153
String imageUrl = collectionImageRepository.findObjectKeysByCollectionId(command.collectionId()).stream()

0 commit comments

Comments
 (0)