Skip to content

Commit 1ccf48e

Browse files
authored
Merge pull request #31 from Team-NextStation/feature/30-place-status-response
[refactor] 비승인 장소 상태 로직 정리
2 parents 159b32b + 21dcb31 commit 1ccf48e

20 files changed

Lines changed: 554 additions & 110 deletions

File tree

‎build.gradle‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dependencies {
6767
annotationProcessor 'org.projectlombok:lombok'
6868

6969
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
70+
testRuntimeOnly 'com.h2database:h2'
7071
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
7172
testImplementation 'org.springframework.boot:spring-boot-starter-validation-test'
7273
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'

‎src/main/java/com/cotato/nextstation/domain/course/converter/CourseConverter.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import com.cotato.nextstation.domain.course.repository.CourseRepository.PopularCourseView;
3434
import com.cotato.nextstation.domain.course.repository.CourseLikeRepository.LikedCourseView;
3535
import com.cotato.nextstation.domain.journal.dto.response.JournalCardInfoResponse;
36-
import com.cotato.nextstation.domain.place.dto.response.PlaceInfoResponse;
36+
import com.cotato.nextstation.domain.place.dto.response.HistoricalPlaceInfoResponse;
3737
import com.cotato.nextstation.domain.station.dto.response.LineSummaryResponse;
3838
import com.cotato.nextstation.domain.station.entity.LineCode;
3939
import org.springframework.stereotype.Component;
@@ -141,7 +141,7 @@ public CourseShareResponse toCourseShareResponse(CourseDetailView course, List<C
141141
);
142142
}
143143

144-
public CoursePlaceDetailResponse toCoursePlaceDetailResponse(PlaceInfoResponse place, int orderNum) {
144+
public CoursePlaceDetailResponse toCoursePlaceDetailResponse(HistoricalPlaceInfoResponse place, int orderNum) {
145145
return new CoursePlaceDetailResponse(
146146
place.placeId(),
147147
place.placeName(),
@@ -151,6 +151,7 @@ public CoursePlaceDetailResponse toCoursePlaceDetailResponse(PlaceInfoResponse p
151151
place.imageUrl(),
152152
place.xCoordinate(),
153153
place.yCoordinate(),
154+
place.placeStatus(),
154155
orderNum
155156
);
156157
}

‎src/main/java/com/cotato/nextstation/domain/course/dto/response/CoursePlaceDetailResponse.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cotato.nextstation.domain.course.dto.response;
22

3+
import com.cotato.nextstation.domain.place.enums.PlaceStatus;
34
import io.swagger.v3.oas.annotations.media.Schema;
45

56
@Schema(description = "코스 확인 화면의 장소 (지도 핀 + 순서 목록에 함께 쓰인다)")
@@ -29,6 +30,9 @@ public record CoursePlaceDetailResponse(
2930
@Schema(description = "위도(y). 지도 핀 좌표", example = "37.5804")
3031
Double yCoordinate,
3132

33+
@Schema(description = "현재 장소 상태. 비승인 상태면 상세 이동과 지도 핀을 제한하는 데 사용한다", example = "APPROVED")
34+
PlaceStatus placeStatus,
35+
3236
@Schema(description = "코스 내 순서. 지도 핀에 찍히는 번호와 같다", example = "1")
3337
int orderNum
3438
) {

‎src/main/java/com/cotato/nextstation/domain/course/service/query/CourseQueryService.java‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.cotato.nextstation.domain.journal.service.query.JournalCardQueryService;
3939
import com.cotato.nextstation.domain.member.exception.MemberErrorCode;
4040
import com.cotato.nextstation.domain.member.service.query.MemberExistenceQueryService;
41+
import com.cotato.nextstation.domain.place.dto.response.HistoricalPlaceInfoResponse;
4142
import com.cotato.nextstation.domain.place.dto.response.PlaceInfoResponse;
4243
import com.cotato.nextstation.domain.place.service.query.PlaceInfoQueryService;
4344
import com.cotato.nextstation.domain.stamp.service.query.MemberStampQueryService;
@@ -59,6 +60,7 @@
5960
import java.util.LinkedHashMap;
6061
import java.util.List;
6162
import java.util.Map;
63+
import java.util.Objects;
6264
import java.util.Set;
6365
import java.util.stream.Collectors;
6466

@@ -550,14 +552,10 @@ private List<CoursePlaceDetailResponse> resolveCoursePlaces(List<CoursePlace> co
550552
}
551553

552554
List<Long> placeIds = coursePlaces.stream().map(CoursePlace::getPlaceId).toList();
553-
Map<Long, PlaceInfoResponse> placeById = placeInfoQueryService.getPlaceInfos(placeIds).stream()
554-
.collect(Collectors.toMap(PlaceInfoResponse::placeId, place -> place));
555-
556-
// 코스에 담긴 장소가 조회되지 않는 건 데이터 정합성이 깨진 상태다(장소 재시딩 등).
557-
// 조용히 빠지면 아무도 모르는 데다, 그 코스는 순서 변경 저장까지 막힌다.
558-
// PATCH /courses/{courseId}가 기존 장소 구성과 정확히 일치할 것을 요구하는데,
559-
// 프론트는 빠진 장소를 모른 채 남은 것만 보내서 INVALID_COURSE_PLACES가 된다.
560-
// 실제로 찍히면 course_places를 정리해야 한다.
555+
Map<Long, HistoricalPlaceInfoResponse> placeById = placeInfoQueryService.getHistoricalPlaceInfos(placeIds).stream()
556+
.collect(Collectors.toMap(HistoricalPlaceInfoResponse::placeId, place -> place));
557+
558+
// 장소가 실제로 사라진 경우만 누락으로 본다. 반려·삭제는 historical 조회가 상태와 함께 반환한다.
561559
if (placeById.size() != coursePlaces.size()) {
562560
List<Long> missingPlaceIds = placeIds.stream()
563561
.filter(placeId -> !placeById.containsKey(placeId))
@@ -566,7 +564,7 @@ private List<CoursePlaceDetailResponse> resolveCoursePlaces(List<CoursePlace> co
566564
coursePlaces.get(0).getCourseId(), missingPlaceIds);
567565
}
568566

569-
// 조회되지 않은 장소는 지도 핀도 못 찍고 목록에도 채울 내용이 없어 제외한다.
567+
// 실제로 삭제된 레코드는 지도 핀과 목록을 채울 정보가 없어 제외한다.
570568
return coursePlaces.stream()
571569
.filter(coursePlace -> placeById.containsKey(coursePlace.getPlaceId()))
572570
.map(coursePlace -> courseConverter.toCoursePlaceDetailResponse(
@@ -620,25 +618,27 @@ private Map<Long, List<Long>> groupPlaceIdsByCourse(List<Long> courseIds) {
620618
));
621619
}
622620

623-
// 카드 배경은 코스의 첫 장소 이미지를 쓴다. 장소 이미지가 없을 때의 폴백은 장소 조회 쪽에서 처리된다.
621+
// 공개 코스 카드의 배경은 코스 순서상 첫 승인 장소 이미지를 사용한다.
624622
private Map<Long, String> resolveCoverImages(Map<Long, List<Long>> placeIdsByCourse) {
625-
Map<Long, Long> firstPlaceByCourse = new LinkedHashMap<>();
626-
placeIdsByCourse.forEach((courseId, placeIds) -> {
627-
if (!placeIds.isEmpty()) {
628-
firstPlaceByCourse.put(courseId, placeIds.get(0));
629-
}
630-
});
631-
if (firstPlaceByCourse.isEmpty()) {
623+
List<Long> candidatePlaceIds = placeIdsByCourse.values().stream()
624+
.flatMap(List::stream)
625+
.distinct()
626+
.toList();
627+
if (candidatePlaceIds.isEmpty()) {
632628
return Map.of();
633629
}
634630

635-
Map<Long, String> imageUrlByPlace = placeInfoQueryService.getPlaceInfos(List.copyOf(firstPlaceByCourse.values()))
631+
Map<Long, String> imageUrlByPlace = placeInfoQueryService.getPlaceInfos(candidatePlaceIds)
636632
.stream()
637633
.filter(place -> place.imageUrl() != null)
638634
.collect(Collectors.toMap(PlaceInfoResponse::placeId, PlaceInfoResponse::imageUrl));
639635

640636
Map<Long, String> result = new LinkedHashMap<>();
641-
firstPlaceByCourse.forEach((courseId, placeId) -> result.put(courseId, imageUrlByPlace.get(placeId)));
637+
placeIdsByCourse.forEach((courseId, placeIds) -> result.put(courseId, placeIds.stream()
638+
.map(imageUrlByPlace::get)
639+
.filter(Objects::nonNull)
640+
.findFirst()
641+
.orElse(null)));
642642
return result;
643643
}
644644

‎src/main/java/com/cotato/nextstation/domain/journal/converter/JournalConverter.java‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import com.cotato.nextstation.domain.journal.repository.JournalRepository.CourseSnapshotView;
1313
import com.cotato.nextstation.domain.journal.repository.JournalRepository.MyJournalCardView;
1414
import com.cotato.nextstation.domain.journal.repository.JournalRepository.UncompletedCourseCardView;
15-
import com.cotato.nextstation.domain.place.dto.response.PlaceInfoResponse;
15+
import com.cotato.nextstation.domain.place.dto.response.HistoricalPlaceInfoResponse;
1616
import com.cotato.nextstation.domain.place.entity.PlaceReview;
17+
import com.cotato.nextstation.domain.place.enums.PlaceStatus;
1718
import com.cotato.nextstation.domain.stamp.entity.MemberStamp;
1819
import com.cotato.nextstation.domain.station.dto.response.LineSummaryResponse;
1920
import org.springframework.stereotype.Component;
@@ -38,15 +39,17 @@ public JournalWriteInfoResponse toWriteInfoResponse(
3839
String courseName,
3940
List<String> tags,
4041
List<CoursePlaceInfoResponse> coursePlaces,
41-
Map<Long, PlaceInfoResponse> placeInfoMap
42+
Map<Long, HistoricalPlaceInfoResponse> placeInfoMap
4243
) {
4344
List<JournalWriteInfoResponse.PlaceSimpleResponse> places = coursePlaces.stream()
4445
.map(cp -> {
45-
PlaceInfoResponse info = placeInfoMap.get(cp.placeId());
46+
HistoricalPlaceInfoResponse info = placeInfoMap.get(cp.placeId());
4647
String placeName = info != null ? info.placeName() : null;
48+
PlaceStatus placeStatus = info != null ? info.placeStatus() : null;
4749

4850
return new JournalWriteInfoResponse.PlaceSimpleResponse(cp.placeId(),
4951
placeName,
52+
placeStatus,
5053
cp.orderNum()
5154
);
5255
} )
@@ -124,7 +127,7 @@ public JournalDetailResponse toJournalDetailResponse(
124127
List<String> tags,
125128
List<JournalImage> journalImages,
126129
List<CoursePlaceInfoResponse> coursePlaces,
127-
Map<Long, PlaceInfoResponse> placeInfoMap,
130+
Map<Long, HistoricalPlaceInfoResponse> placeInfoMap,
128131
Map<Long, PlaceReview> reviewByPlaceId,
129132
Map<Long, String> imageUrlByReviewId
130133
) {
@@ -166,13 +169,13 @@ public JournalDetailResponse toJournalDetailResponse(
166169

167170
private List<JournalDetailResponse.VisitedPlaceResponse> toVisitedPlaceResponses(
168171
List<CoursePlaceInfoResponse> coursePlaces,
169-
Map<Long, PlaceInfoResponse> placeInfoMap,
172+
Map<Long, HistoricalPlaceInfoResponse> placeInfoMap,
170173
Map<Long, PlaceReview> reviewByPlaceId,
171174
Map<Long, String> imageUrlByReviewId
172175
) {
173176
return coursePlaces.stream()
174177
.map(cp -> {
175-
PlaceInfoResponse placeInfo = placeInfoMap.get(cp.placeId());
178+
HistoricalPlaceInfoResponse placeInfo = placeInfoMap.get(cp.placeId());
176179
PlaceReview review = reviewByPlaceId.get(cp.placeId());
177180
String reviewImageUrl = review != null
178181
? imageUrlByReviewId.get(review.getId())
@@ -184,10 +187,11 @@ private List<JournalDetailResponse.VisitedPlaceResponse> toVisitedPlaceResponses
184187
placeInfo != null ? placeInfo.placeName() : null,
185188
placeInfo != null ? placeInfo.xCoordinate() : null,
186189
placeInfo != null ? placeInfo.yCoordinate() : null,
190+
placeInfo != null ? placeInfo.placeStatus() : null,
187191
review != null ? review.getReview() : null,
188192
reviewImageUrl
189193
);
190194
})
191195
.toList();
192196
}
193-
}
197+
}

‎src/main/java/com/cotato/nextstation/domain/journal/dto/response/JournalDetailResponse.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cotato.nextstation.domain.journal.dto.response;
22

33
import com.cotato.nextstation.domain.journal.enums.TravelDuration;
4+
import com.cotato.nextstation.domain.place.enums.PlaceStatus;
45
import com.cotato.nextstation.domain.station.dto.response.LineSummaryResponse;
56
import io.swagger.v3.oas.annotations.media.Schema;
67

@@ -85,7 +86,10 @@ public record VisitedPlaceResponse(
8586
@Schema(description = "y좌표", example = "37.456")
8687
Double yCoordinate,
8788

89+
@Schema(description = "현재 장소 상태. 비승인 상태면 상세 이동과 지도 핀을 제한하는 데 사용한다", example = "APPROVED")
90+
PlaceStatus placeStatus,
91+
8892
String review,
8993
String imageUrl
9094
) {}
91-
}
95+
}

‎src/main/java/com/cotato/nextstation/domain/journal/dto/response/JournalWriteInfoResponse.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.cotato.nextstation.domain.journal.dto.response;
22

33
import com.cotato.nextstation.domain.course.dto.response.CoursePlaceInfoResponse;
4-
import com.cotato.nextstation.domain.place.dto.response.PlaceInfoResponse;
4+
import com.cotato.nextstation.domain.place.enums.PlaceStatus;
55
import io.swagger.v3.oas.annotations.media.Schema;
66

77
import java.util.List;
@@ -25,6 +25,7 @@ public record JournalWriteInfoResponse(
2525
public record PlaceSimpleResponse(
2626
Long placeId,
2727
String placeName,
28+
PlaceStatus placeStatus,
2829
int orderNum
2930
) {}
30-
}
31+
}

‎src/main/java/com/cotato/nextstation/domain/journal/service/query/JournalQueryService.java‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import com.cotato.nextstation.domain.journal.repository.JournalRepository.CourseSnapshotView;
2222
import com.cotato.nextstation.domain.journal.repository.JournalRepository.MyJournalCardView;
2323
import com.cotato.nextstation.domain.journal.repository.JournalRepository.UncompletedCourseCardView;
24-
import com.cotato.nextstation.domain.place.dto.response.PlaceInfoResponse;
24+
import com.cotato.nextstation.domain.place.dto.response.HistoricalPlaceInfoResponse;
2525
import com.cotato.nextstation.domain.place.entity.PlaceReview;
2626
import com.cotato.nextstation.domain.place.entity.PlaceReviewImage;
2727
import com.cotato.nextstation.domain.place.repository.PlaceReviewImageRepository;
@@ -92,9 +92,9 @@ public JournalWriteInfoResponse getWriteInfo(Long memberId, Long memberStampId)
9292
.toList();
9393

9494
// 5. placeIds → 장소 이름
95-
Map<Long, PlaceInfoResponse> placeInfoMap = placeInfoQueryService.getPlaceInfos(placeIds)
95+
Map<Long, HistoricalPlaceInfoResponse> placeInfoMap = placeInfoQueryService.getHistoricalPlaceInfos(placeIds)
9696
.stream()
97-
.collect(Collectors.toMap(PlaceInfoResponse::placeId, Function.identity()));
97+
.collect(Collectors.toMap(HistoricalPlaceInfoResponse::placeId, Function.identity()));
9898

9999
// 6. placeIds → 태그 상위 3개
100100
List<String> tags = placeInfoQueryService.getTopTagNames(placeIds);
@@ -270,9 +270,9 @@ public JournalDetailResponse getJournalDetail(Long memberId, Long journalId) {
270270
.toList();
271271

272272
// 7. placeIds → 장소 이름
273-
Map<Long, PlaceInfoResponse> placeInfoMap = placeInfoQueryService.getPlaceInfos(placeIds)
273+
Map<Long, HistoricalPlaceInfoResponse> placeInfoMap = placeInfoQueryService.getHistoricalPlaceInfos(placeIds)
274274
.stream()
275-
.collect(Collectors.toMap(PlaceInfoResponse::placeId, Function.identity()));
275+
.collect(Collectors.toMap(HistoricalPlaceInfoResponse::placeId, Function.identity()));
276276

277277
// 8. placeIds → 태그 상위 3개
278278
List<String> tags = placeInfoQueryService.getTopTagNames(placeIds);
@@ -283,8 +283,17 @@ public JournalDetailResponse getJournalDetail(Long memberId, Long journalId) {
283283

284284
// 10. journalId → 장소 리뷰 + 리뷰 이미지
285285
List<PlaceReview> placeReviews = placeReviewRepository.findByJournalId(journalId);
286+
Map<Long, Long> placeIdByReviewId = placeReviewRepository.findReviewPlaceIdsByJournalId(journalId).stream()
287+
.collect(Collectors.toMap(
288+
PlaceReviewRepository.ReviewPlaceView::getReviewId,
289+
PlaceReviewRepository.ReviewPlaceView::getPlaceId
290+
));
286291
Map<Long, PlaceReview> reviewByPlaceId = placeReviews.stream()
287-
.collect(Collectors.toMap(pr -> pr.getPlace().getId(), Function.identity()));
292+
.filter(review -> placeIdByReviewId.containsKey(review.getId()))
293+
.collect(Collectors.toMap(
294+
review -> placeIdByReviewId.get(review.getId()),
295+
Function.identity()
296+
));
288297

289298
List<Long> reviewIds = placeReviews.stream().map(PlaceReview::getId).toList();
290299
Map<Long, String> imageUrlByReviewId = placeReviewImageRepository
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.cotato.nextstation.domain.place.dto.response;
2+
3+
import com.cotato.nextstation.domain.place.enums.PlaceStatus;
4+
5+
/**
6+
* 코스·여행일지처럼 이미 저장된 기록을 복원할 때 사용하는 장소 정보다.
7+
* 일반 사용자 장소 조회와 달리 비승인 장소도 상태를 포함해 반환한다.
8+
*/
9+
public record HistoricalPlaceInfoResponse(
10+
Long placeId,
11+
String placeName,
12+
String description,
13+
String categoryCode,
14+
String categoryName,
15+
String imageUrl,
16+
Double xCoordinate,
17+
Double yCoordinate,
18+
PlaceStatus placeStatus
19+
) {
20+
}

‎src/main/java/com/cotato/nextstation/domain/place/repository/PlaceRepository.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,30 @@ public interface PlaceRepository extends JpaRepository<Place, Long> {
1111

1212
List<Place> findByStationId(Long stationId);
1313

14+
/**
15+
* 이미 코스·여행일지에 저장된 장소를 복원하는 전용 조회다.
16+
* Place 엔티티의 @SQLRestriction을 적용하지 않아 비승인 장소도 상태와 함께 읽는다.
17+
*/
18+
@Query(value = """
19+
SELECT p.id AS placeId,
20+
p.place_name AS placeName,
21+
p.description AS description,
22+
c.code AS categoryCode,
23+
c.name AS categoryName,
24+
COALESCE((SELECT pi.image_url
25+
FROM place_image pi
26+
WHERE pi.place_id = p.id
27+
ORDER BY pi.sort_order, pi.id
28+
LIMIT 1), c.default_image_url) AS imageUrl,
29+
p.x_coordinate AS xCoordinate,
30+
p.y_coordinate AS yCoordinate,
31+
p.status AS placeStatus
32+
FROM place p
33+
JOIN category c ON c.id = p.category_id
34+
WHERE p.id IN (:placeIds)
35+
""", nativeQuery = true)
36+
List<HistoricalPlaceView> findHistoricalPlacesByIdIn(@Param("placeIds") List<Long> placeIds);
37+
1438
default boolean existsByStationAndKakaoPlaceIdForAdmin(Long stationId, String kakaoPlaceId) {
1539
return countByStationAndKakaoPlaceIdForAdmin(stationId, kakaoPlaceId) > 0;
1640
}
@@ -19,4 +43,16 @@ default boolean existsByStationAndKakaoPlaceIdForAdmin(Long stationId, String ka
1943
nativeQuery = true)
2044
long countByStationAndKakaoPlaceIdForAdmin(@Param("stationId") Long stationId,
2145
@Param("kakaoPlaceId") String kakaoPlaceId);
46+
47+
interface HistoricalPlaceView {
48+
Long getPlaceId();
49+
String getPlaceName();
50+
String getDescription();
51+
String getCategoryCode();
52+
String getCategoryName();
53+
String getImageUrl();
54+
Double getXCoordinate();
55+
Double getYCoordinate();
56+
String getPlaceStatus();
57+
}
2258
}

0 commit comments

Comments
 (0)