Skip to content

Commit 8a28f76

Browse files
authored
Merge pull request #27 from 9oormthon-univ/feature/district
[Feat] 자치구 + TrashType에 따른 요일 출력
2 parents 63baee5 + 502756e commit 8a28f76

File tree

5 files changed

+207
-94
lines changed

5 files changed

+207
-94
lines changed

src/main/java/com/trashheroesbe/feature/disposal/infrastructure/DisposalRepository.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,33 @@
22

33
import com.trashheroesbe.feature.disposal.domain.Disposal;
44
import com.trashheroesbe.feature.district.domain.entity.District;
5-
import java.util.List;
5+
import com.trashheroesbe.feature.trash.domain.Type;
66
import org.springframework.data.jpa.repository.EntityGraph;
7+
import org.springframework.data.jpa.repository.JpaRepository;
78
import org.springframework.data.jpa.repository.Query;
8-
import org.springframework.data.repository.CrudRepository;
99
import org.springframework.data.repository.query.Param;
1010

11-
public interface DisposalRepository extends CrudRepository<Disposal, Long> {
11+
import java.util.List;
12+
import java.util.Optional;
13+
14+
public interface DisposalRepository extends JpaRepository<Disposal, Long> {
15+
16+
// 1) 기존 용도: 자치구+타입(enum)으로 단건 days 조회
17+
@EntityGraph(attributePaths = {"district", "trashType"})
18+
Optional<Disposal> findByDistrict_IdAndTrashType_Type(String districtId, Type type);
1219

20+
// 2) 추가 용도: 자치구로 전체 가져와서 서비스에서 타입 필터링이 필요할 때
1321
@EntityGraph(attributePaths = {"district", "trashType"})
1422
List<Disposal> findByDistrict(District district);
1523

24+
// 3) 추가 용도: 자치구의 특정 요일 배출 항목들 조회(MySQL 8+ 전제)
1625
@EntityGraph(attributePaths = {"district", "trashType"})
1726
@Query("""
1827
select d
1928
from Disposal d
2029
where d.district.id = :districtId
2130
and function('json_overlaps', d.days, function('json_array', :day)) = true
2231
""")
23-
List<Disposal> findAllByDistrictAndDay(
24-
@Param("districtId") String districtId,
25-
@Param("day") String day
26-
);
27-
}
32+
List<Disposal> findAllByDistrictAndDay(@Param("districtId") String districtId,
33+
@Param("day") String day);
34+
}

src/main/java/com/trashheroesbe/feature/trash/api/TrashControllerApi.java

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,35 +35,49 @@ public interface TrashControllerApi {
3535
description = "성공",
3636
content = @Content(
3737
mediaType = "application/json",
38-
schema = @Schema(implementation = TrashResultResponse.class),
38+
schema = @Schema(implementation = ApiResponse.class),
3939
examples = @ExampleObject(name = "success", value = """
40-
{
41-
"httpCode": 200,
42-
"httpStatus": "OK",
43-
"message": "요청에 성공하였습니다.",
44-
"data": {
45-
"id": 8,
46-
"imageUrl": "https://trashheroes.s3.ap-northeast-2.amazonaws.com/trash/20250904_003905_126ed9a4.jpg",
47-
"name": "쓰레기",
48-
"summary": "생수용 PET 플라스틱 병(뚜껑 포함)",
49-
"itemName": "PET(투명 페트병)",
50-
"typeCode": "R04",
51-
"typeName": "PET(투명 페트병)",
52-
"guideSteps": [
53-
"STEP 1: 내용물을 비우고 깨끗이 헹궈요.",
54-
"STEP 2: 비닐 라벨을 제거하여 비닐류로 배출해요.",
55-
"STEP 3: 페트병은 찌그러뜨린 뒤 뚜껑을 닫아 일반 플라스틱류와 구분하여 투명/반투명 봉투에 담아 배출해요."
56-
],
57-
"cautionNote": "주의: 유색·불투명 페트병이나 식용유병은 투명 페트병으로 분리하지 않아요.",
58-
"parts": [
59-
{ "name": "페트병 뚜껑", "typeCode": "R04", "typeName": "PET(투명 페트병)" },
60-
{ "name": "투명 페트병 몸체", "typeCode": "R04", "typeName": "PET(투명 페트병)" },
61-
{ "name": "비닐 라벨", "typeCode": "R05", "typeName": "비닐류" }
62-
],
63-
"createdAt": "2025-09-04T00:39:17.853269"
64-
}
65-
}
66-
""")
40+
{
41+
"httpCode": 200,
42+
"httpStatus": "OK",
43+
"message": "요청에 성공하였습니다.",
44+
"data": {
45+
"id": 8,
46+
"imageUrl": "https://trashheroes.s3.ap-northeast-2.amazonaws.com/trash/20250904_003905_126ed9a4.jpg",
47+
"name": "쓰레기",
48+
"summary": "생수용 PET 플라스틱 병(뚜껑 포함)",
49+
"itemName": "PET(투명 페트병)",
50+
"typeCode": "R04",
51+
"typeName": "PET(투명 페트병)",
52+
"guideSteps": [
53+
"STEP 1: 내용물을 비우고 깨끗이 헹궈요.",
54+
"STEP 2: 비닐 라벨을 제거하여 비닐류로 배출해요.",
55+
"STEP 3: 페트병은 찌그러뜨린 뒤 뚜껑을 닫아 일반 플라스틱류와 구분하여 투명/반투명 봉투에 담아 배출해요."
56+
],
57+
"cautionNote": "주의: 유색·불투명 페트병이나 식용유병은 투명 페트병으로 분리하지 않아요.",
58+
"days": [
59+
"월요일",
60+
"화요일",
61+
"수요일",
62+
"목요일",
63+
"금요일",
64+
"일요일"
65+
],
66+
"location": {
67+
"id": "1156012500",
68+
"sido": "서울특별시",
69+
"sigungu": "영등포구",
70+
"eupmyeondong": "양평동1가"
71+
},
72+
"parts": [
73+
{ "name": "페트병 뚜껑", "typeCode": "R04", "typeName": "PET(투명 페트병)" },
74+
{ "name": "투명 페트병 몸체", "typeCode": "R04", "typeName": "PET(투명 페트병)" },
75+
{ "name": "비닐 라벨", "typeCode": "R05", "typeName": "비닐류" }
76+
],
77+
"createdAt": "2025-09-04T00:39:17.853269"
78+
}
79+
}
80+
""")
6781
)
6882
)
6983
ApiResponse<TrashResultResponse> createTrash(CreateTrashRequest request, @AuthenticationPrincipal CustomerDetails customerDetails);
@@ -93,16 +107,16 @@ public interface TrashControllerApi {
93107
mediaType = "application/json",
94108
schema = @Schema(implementation = TrashResultResponse.class),
95109
examples = @ExampleObject(name = "success", value = """
96-
{
97-
"httpCode": 200,
98-
"httpStatus": "OK",
99-
"message": "요청에 성공하였습니다.",
100-
"data": [
101-
{ "id": 8, "imageUrl": "...", "name": "쓰레기", "summary": "..." },
102-
{ "id": 7, "imageUrl": "...", "name": "쓰레기", "summary": "..." }
103-
]
104-
}
105-
""")
110+
{
111+
"httpCode": 200,
112+
"httpStatus": "OK",
113+
"message": "요청에 성공하였습니다.",
114+
"data": [
115+
{ "id": 8, "imageUrl": "...", "name": "쓰레기", "summary": "..." },
116+
{ "id": 7, "imageUrl": "...", "name": "쓰레기", "summary": "..." }
117+
]
118+
}
119+
""")
106120
)
107121
)
108122
ApiResponse<List<TrashResultResponse>> getMyTrash(@AuthenticationPrincipal CustomerDetails customerDetails);
@@ -114,13 +128,13 @@ public interface TrashControllerApi {
114128
content = @Content(
115129
mediaType = "application/json",
116130
examples = @ExampleObject(value = """
117-
{
118-
"httpCode": 200,
119-
"httpStatus": "OK",
120-
"message": "요청에 성공하였습니다.",
121-
"data": null
122-
}
123-
""")
131+
{
132+
"httpCode": 200,
133+
"httpStatus": "OK",
134+
"message": "요청에 성공하였습니다.",
135+
"data": null
136+
}
137+
""")
124138
)
125139
)
126140
ApiResponse<Void> deleteTrash(@PathVariable Long trashId, @AuthenticationPrincipal CustomerDetails customerDetails);

src/main/java/com/trashheroesbe/feature/trash/application/TrashService.java

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
package com.trashheroesbe.feature.trash.application;
22

3-
import com.trashheroesbe.feature.trash.dto.response.PartCardResponse;
4-
import com.trashheroesbe.feature.trash.dto.response.TrashAnalysisResponseDto;
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.trashheroesbe.feature.disposal.infrastructure.DisposalRepository;
5+
import com.trashheroesbe.feature.trash.dto.response.*;
56
import com.trashheroesbe.feature.trash.domain.entity.TrashDescription;
67
import com.trashheroesbe.feature.trash.domain.Type;
78
import com.trashheroesbe.feature.trash.domain.entity.Trash;
89
import com.trashheroesbe.feature.trash.domain.entity.TrashItem;
910
import com.trashheroesbe.feature.trash.domain.entity.TrashType;
1011
import com.trashheroesbe.feature.trash.dto.request.CreateTrashRequest;
11-
import com.trashheroesbe.feature.trash.dto.response.TrashItemResponse;
12-
import com.trashheroesbe.feature.trash.dto.response.TrashResultResponse;
1312
import com.trashheroesbe.feature.trash.infrastructure.TrashDescriptionRepository;
1413
import com.trashheroesbe.feature.trash.infrastructure.TrashItemRepository;
1514
import com.trashheroesbe.feature.trash.infrastructure.TrashRepository;
1615
import com.trashheroesbe.feature.trash.infrastructure.TrashTypeRepository;
1716
import com.trashheroesbe.feature.user.domain.entity.User;
17+
import com.trashheroesbe.feature.user.infrastructure.UserDistrictRepository;
1818
import com.trashheroesbe.global.exception.BusinessException;
1919
import com.trashheroesbe.global.response.type.ErrorCode;
2020
import com.trashheroesbe.global.util.FileUtils;
@@ -43,6 +43,8 @@ public class TrashService implements TrashCreateUseCase {
4343
private final TrashTypeRepository trashTypeRepository;
4444
private final TrashItemRepository trashItemRepository;
4545
private final TrashDescriptionRepository trashDescriptionRepository;
46+
private final DisposalRepository disposalRepository;
47+
private final UserDistrictRepository userDistrictRepository;
4648

4749
@Override
4850
@Transactional
@@ -53,7 +55,7 @@ public TrashResultResponse createTrash(CreateTrashRequest request, User user) {
5355
try {
5456
var file = request.imageFile();
5557
String storedKey = FileUtils.generateStoredKey(
56-
Objects.requireNonNull(file.getOriginalFilename()), S3_TRASH_PREFIX);
58+
Objects.requireNonNull(file.getOriginalFilename()), S3_TRASH_PREFIX);
5759
byte[] bytes = file.getBytes();
5860
String contentType = file.getContentType();
5961

@@ -62,12 +64,12 @@ public TrashResultResponse createTrash(CreateTrashRequest request, User user) {
6264

6365
// 2) 타입 결정
6466
Type analyzedType =
65-
(step1 != null && step1.type() != null && step1.type().getType() != null)
66-
? step1.type().getType() : Type.UNKNOWN;
67+
(step1 != null && step1.type() != null && step1.type().getType() != null)
68+
? step1.type().getType() : Type.UNKNOWN;
6769

68-
// 3) 타입 엔티티 조회/없으면 생성 → 여기서 'type' 정의
70+
// 3) 타입 엔티티 조회/없으면 생성
6971
TrashType type = trashTypeRepository.findByType(analyzedType)
70-
.orElseGet(() -> trashTypeRepository.save(TrashType.of(analyzedType)));
72+
.orElseGet(() -> trashTypeRepository.save(TrashType.of(analyzedType)));
7173

7274
// 4) 재활용군이면 세부 품목 분석
7375
String itemName = null;
@@ -86,26 +88,35 @@ public TrashResultResponse createTrash(CreateTrashRequest request, User user) {
8688
if (itemName != null && !itemName.isBlank()) {
8789
String key = itemName.trim();
8890
var item = trashItemRepository.findByTrashTypeAndName(type, key)
89-
.orElseGet(() -> trashItemRepository.save(TrashItem.builder()
90-
.trashType(type)
91-
.name(key)
92-
.build()));
91+
.orElseGet(() -> trashItemRepository.save(TrashItem.builder()
92+
.trashType(type)
93+
.name(key)
94+
.build()));
9395
trash.applyItem(item);
9496
}
9597

9698
Trash saved = trashRepository.save(trash);
9799

98100
// 가이드/주의사항 조회
99101
var descOpt = trashDescriptionRepository.findByTrashType(type);
100-
var steps = descOpt.map(TrashDescription::steps).orElse(java.util.List.of());
102+
var steps = descOpt.map(TrashDescription::steps).orElse(List.of());
101103
var caution = descOpt.map(TrashDescription::getCautionNote).orElse(null);
102104

105+
// 부품 카드
103106
var parts = suggestParts(type.getType());
104107

108+
// 사용자 기본 자치구/배출요일
109+
var district = resolveUserDistrictSummary(user.getId());
110+
105111
log.info("쓰레기 생성 완료: id={}, userId={}, type={}, imageUrl={}",
106-
saved.getId(), user.getId(), type.getType(), imageUrl);
112+
saved.getId(), user.getId(), type.getType(), imageUrl);
113+
114+
List<String> days = java.util.Collections.emptyList();
115+
if (district != null) {
116+
days = resolveDisposalDays(district.id(), TrashType.of(type.getType()));
117+
}
107118

108-
return TrashResultResponse.of(saved, steps, caution, parts);
119+
return TrashResultResponse.of(saved, steps, caution, days, parts, district);
109120

110121
} catch (BusinessException be) {
111122
throw be;
@@ -135,6 +146,39 @@ private List<PartCardResponse> suggestParts(Type baseType) {
135146
return List.of();
136147
}
137148

149+
private List<String> parseDays(String json) {
150+
if (json == null || json.isBlank()) return List.of();
151+
try {
152+
var node = new ObjectMapper().readTree(json);
153+
if (!node.isArray()) return List.of();
154+
List<String> out = new java.util.ArrayList<>();
155+
node.forEach(n -> out.add(n.asText()));
156+
return out;
157+
} catch (Exception e) {
158+
return List.of();
159+
}
160+
}
161+
162+
private DistrictSummaryResponse resolveUserDistrictSummary(Long userId) {
163+
var uds = userDistrictRepository.findByUserIdFetchJoin(userId);
164+
var udOpt = uds.stream()
165+
.filter(ud -> Boolean.TRUE.equals(ud.getIsDefault()))
166+
.findFirst()
167+
.or(() -> uds.stream().findFirst());
168+
return udOpt.map(ud -> {
169+
var d = ud.getDistrict();
170+
return new DistrictSummaryResponse(d.getId(), d.getSido(), d.getSigungu(), d.getEupmyeondong());
171+
}).orElse(null);
172+
}
173+
174+
private List<String> resolveDisposalDays(String districtId, TrashType trashType) {
175+
if (districtId == null || trashType == null) return List.of();
176+
return disposalRepository
177+
.findByDistrict_IdAndTrashType_Type(districtId, trashType.getType())
178+
.map(d -> parseDays(d.getDays()))
179+
.orElse(List.of());
180+
}
181+
138182
/**
139183
* 특정 사용자의 모든 쓰레기를 최신순으로 조회
140184
*/
@@ -156,14 +200,37 @@ public TrashResultResponse getTrash(Long trashId) {
156200
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_EXISTS_TRASH_ITEM));
157201

158202
var descOpt = trashDescriptionRepository.findByTrashType(trash.getTrashType());
159-
var steps = descOpt.map(TrashDescription::steps).orElse(java.util.List.of());
160-
var caution = descOpt.map(TrashDescription::getCautionNote).orElse(null);
203+
var steps = descOpt.map(TrashDescription::steps)
204+
.orElse(java.util.List.of());
205+
var caution = descOpt.map(TrashDescription::getCautionNote)
206+
.orElse(null);
161207

162208
var parts = suggestParts(
163209
trash.getTrashType() != null ? trash.getTrashType().getType() : Type.UNKNOWN
164210
);
165211

166-
return TrashResultResponse.of(trash, steps, caution, parts);
212+
// 사용자 기본 자치구 요약
213+
var uds = userDistrictRepository.findByUserIdFetchJoin(trash.getUser().getId());
214+
var districtOpt = uds.stream()
215+
.filter(ud -> Boolean.TRUE.equals(ud.getIsDefault()))
216+
.findFirst()
217+
.or(() -> uds.stream().findFirst());
218+
219+
DistrictSummaryResponse district = districtOpt.map(ud -> {
220+
var d = ud.getDistrict();
221+
return new DistrictSummaryResponse(d.getId(), d.getSido(), d.getSigungu(), d.getEupmyeondong());
222+
}).orElse(null);
223+
224+
// 자치구 + 타입(enum)으로 days 조회 (enum 기반)
225+
java.util.List<String> days = java.util.Collections.emptyList();
226+
if (district != null && trash.getTrashType() != null) {
227+
days = disposalRepository
228+
.findByDistrict_IdAndTrashType_Type(district.id(), trash.getTrashType().getType())
229+
.map(d -> parseDays(d.getDays()))
230+
.orElse(java.util.List.of());
231+
}
232+
233+
return TrashResultResponse.of(trash, steps, caution, days, parts, district);
167234
}
168235

169236
@Transactional(readOnly = true)
@@ -201,14 +268,19 @@ public TrashResultResponse changeTrashItem(Long trashId, Long trashItemId, User
201268
trash.applyItem(item);
202269

203270
var descOpt = trashDescriptionRepository.findByTrashType(trash.getTrashType());
204-
var steps = descOpt.map(TrashDescription::steps)
205-
.orElse(java.util.List.of());
206-
var caution = descOpt.map(TrashDescription::getCautionNote)
207-
.orElse(null);
271+
var steps = descOpt.map(TrashDescription::steps).orElse(java.util.List.of());
272+
var caution = descOpt.map(TrashDescription::getCautionNote).orElse(null);
208273

209274
var parts = suggestParts(trash.getTrashType().getType());
210275

211-
return TrashResultResponse.of(trash, steps, caution, parts);
276+
// 사용자 기본 자치구 + 배출 요일(enum 기반)
277+
var district = resolveUserDistrictSummary(user.getId());
278+
java.util.List<String> days = java.util.Collections.emptyList();
279+
if (district != null && trash.getTrashType() != null) {
280+
days = resolveDisposalDays(district.id(), TrashType.of(trash.getTrashType().getType()));
281+
}
282+
283+
return TrashResultResponse.of(trash, steps, caution, days, parts, district);
212284
}
213285

214286
@Transactional
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.trashheroesbe.feature.trash.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "사용자 기본 자치구")
6+
public record DistrictSummaryResponse(
7+
@Schema(description = "자치구 ID", example = "1100053")
8+
String id,
9+
@Schema(description = "시/도", example = "서울특별시")
10+
String sido,
11+
@Schema(description = "시/군/구", example = "강남구")
12+
String sigungu,
13+
@Schema(description = "읍/면/동", example = "역삼동")
14+
String eupmyeondong
15+
) {}

0 commit comments

Comments
 (0)