Skip to content

Commit 12e2137

Browse files
authored
Merge pull request #1 from GarakChoi/feat/22-post
feat[Law]:total search
2 parents 9c4568a + 78377f5 commit 12e2137

File tree

13 files changed

+287
-5
lines changed

13 files changed

+287
-5
lines changed

backend/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies {
4242
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'
4343
implementation 'org.springframework.boot:spring-boot-starter-batch'
4444
testImplementation 'org.springframework.kafka:spring-kafka-test'
45+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
4546

4647
// API Documentation (문서화)
4748
implementation 'org.apache.commons:commons-lang3:3.18.0'

backend/sql/precedent_fulltext.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE precedent
2+
ADD FULLTEXT idx_precedent_fulltext (notice, summary_of_the_judgment, precedent_content, case_name, case_number);

backend/src/main/java/com/ai/lawyer/domain/law/controller/LawController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@ public ResponseEntity<?> getFullLaw(@PathVariable Long id) {
6666

6767

6868
}
69-
}
69+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.ai.lawyer.domain.poll.event;
2+
3+
import com.ai.lawyer.domain.poll.dto.PollVoteDto;
4+
5+
public class PollVotedEvent {
6+
private final Long pollId;
7+
private final PollVoteDto voteDto;
8+
9+
public PollVotedEvent(Long pollId, PollVoteDto voteDto) {
10+
this.pollId = pollId;
11+
this.voteDto = voteDto;
12+
}
13+
14+
public Long getPollId() {
15+
return pollId;
16+
}
17+
18+
public PollVoteDto getVoteDto() {
19+
return voteDto;
20+
}
21+
}
22+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.ai.lawyer.domain.poll.event;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.messaging.simp.SimpMessagingTemplate;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.event.TransactionPhase;
8+
import org.springframework.transaction.event.TransactionalEventListener;
9+
10+
@Component
11+
@RequiredArgsConstructor
12+
@Slf4j
13+
public class PollVotedEventListener {
14+
15+
private final SimpMessagingTemplate messagingTemplate;
16+
17+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
18+
public void onPollVoted(PollVotedEvent event) {
19+
if (event == null || event.getVoteDto() == null) return;
20+
String destination = "/topic/poll." + event.getPollId();
21+
try {
22+
// 이벤트 도착 로그
23+
log.debug("PollVotedEvent received for pollId={}, destination={}", event.getPollId(), destination);
24+
log.trace("Payload: {}", event.getVoteDto());
25+
26+
messagingTemplate.convertAndSend(destination, event.getVoteDto());
27+
28+
// 전송 완료 로그
29+
log.debug("WebSocket message sent to destination={}", destination);
30+
} catch (Exception e) {
31+
log.warn("투표({}) 웹소켓 메시지 전송 실패", event.getPollId(), e);
32+
}
33+
}
34+
}

backend/src/main/java/com/ai/lawyer/domain/poll/service/PollServiceImpl.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import com.ai.lawyer.global.util.AuthUtil;
2424
import org.springframework.dao.DataIntegrityViolationException;
25+
import org.springframework.context.ApplicationEventPublisher;
26+
import com.ai.lawyer.domain.poll.event.PollVotedEvent;
2527

2628
@Service
2729
@Transactional
@@ -34,6 +36,7 @@ public class PollServiceImpl implements PollService {
3436
private final PollVoteRepository pollVoteRepository;
3537
private final PollStaticsRepository pollStaticsRepository;
3638
private final PostRepository postRepository;
39+
private final ApplicationEventPublisher applicationEventPublisher;
3740

3841
@Override
3942
public PollDto createPoll(PollCreateDto request, Long memberId) {
@@ -120,7 +123,12 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) {
120123
// 1) 기존 투표가 있는지 조회
121124
var existingOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
122125
if (existingOpt.isPresent()) {
123-
return handleExistingVote(existingOpt.get(), pollOptions, pollId, pollItemsId, memberId);
126+
PollVoteDto result = handleExistingVote(existingOpt.get(), pollOptions, pollId, pollItemsId, memberId);
127+
// idempotent 응답(같은 항목)인 경우에는 브로드캐스트하지 않음
128+
if (!"이미 해당 항목에 투표하셨습니다.".equals(result.getMessage())) {
129+
applicationEventPublisher.publishEvent(new PollVotedEvent(pollId, result));
130+
}
131+
return result;
124132
}
125133
// 2) 신규 투표 생성
126134
PollVote newVote = PollVote.builder()
@@ -129,13 +137,19 @@ public PollVoteDto vote(Long pollId, Long pollItemsId, Long memberId) {
129137
.memberId(memberId)
130138
.build();
131139
PollVote saved = pollVoteRepository.save(newVote);
132-
return buildPollVote(saved, pollId, pollItemsId, memberId, "투표가 완료되었습니다.");
140+
PollVoteDto dto = buildPollVote(saved, pollId, pollItemsId, memberId, "투표가 완료되었습니다.");
141+
applicationEventPublisher.publishEvent(new PollVotedEvent(pollId, dto));
142+
return dto;
133143
} catch (DataIntegrityViolationException e) {
134144
// 동시성(경합)으로 인해 이미 다른 쓰레드가 투표를 만들어 중복 제약에 걸린 경우 복구 처리
135145
log.warn("중복 투표 시도 감지 - memberId: {}, pollId: {}", memberId, pollId, e);
136146
var existingAfterOpt = pollVoteRepository.findByMemberIdAndPoll_PollId(memberId, pollId);
137147
if (existingAfterOpt.isPresent()) {
138-
return handleExistingVote(existingAfterOpt.get(), pollOptions, pollId, pollItemsId, memberId);
148+
PollVoteDto result = handleExistingVote(existingAfterOpt.get(), pollOptions, pollId, pollItemsId, memberId);
149+
if (!"이미 해당 항목에 투표하셨습니다.".equals(result.getMessage())) {
150+
applicationEventPublisher.publishEvent(new PollVotedEvent(pollId, result));
151+
}
152+
return result;
139153
}
140154
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 투표하셨습니다. 중복 투표는 불가능합니다.");
141155
}

backend/src/main/java/com/ai/lawyer/domain/precedent/service/PrecedentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,4 @@ private void parseSentencingDate(String dateStr, Precedent precedent) {
375375
}
376376
}
377377
}
378-
}
378+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.ai.lawyer.domain.search.controller;
2+
3+
import com.ai.lawyer.domain.search.dto.SearchRequestDto;
4+
import com.ai.lawyer.domain.search.dto.SearchResponseDto;
5+
import com.ai.lawyer.domain.search.service.SearchService;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@Slf4j
17+
@RestController
18+
@RequiredArgsConstructor
19+
@RequestMapping("/api/search")
20+
@Tag(name = "통합 검색", description = "법령 + 판례 통합 검색 API")
21+
public class SearchController {
22+
23+
private final SearchService searchService;
24+
25+
@PostMapping("/combined")
26+
@Operation(summary = "법령 + 판례 통합 검색", description = "법령과 판례를 함께 검색합니다. includeLaws/includePrecedents 플래그로 선택 가능합니다.")
27+
public ResponseEntity<?> combinedSearch(@RequestBody SearchRequestDto request) {
28+
try {
29+
SearchResponseDto response = searchService.combinedSearch(request);
30+
return ResponseEntity.ok(response);
31+
} catch (Exception e) {
32+
log.error("통합 검색 에러 : {}", e.getMessage(), e);
33+
return ResponseEntity.badRequest().body("통합 검색 에러 : " + e.getMessage());
34+
}
35+
}
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.ai.lawyer.domain.search.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
@Data
10+
@Builder
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class SearchRequestDto {
14+
15+
@Schema(description = "검색 키워드", example = "형사")
16+
private String keyword;
17+
18+
@Schema(description = "페이지 번호 (0부터 시작)", example = "0")
19+
private int pageNumber = 0;
20+
21+
@Schema(description = "페이지 크기", example = "10")
22+
private int pageSize = 10;
23+
24+
@Schema(description = "법령 검색 포함 여부")
25+
private boolean includeLaws = true;
26+
27+
@Schema(description = "판례 검색 포함 여부")
28+
private boolean includePrecedents = true;
29+
}
30+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.ai.lawyer.domain.search.dto;
2+
3+
import com.ai.lawyer.global.dto.PageResponseDto;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class SearchResponseDto {
15+
16+
@Schema(description = "법령 검색 결과 페이지 (없으면 null)")
17+
private PageResponseDto laws;
18+
19+
@Schema(description = "판례 검색 결과 페이지 (없으면 null)")
20+
private PageResponseDto precedents;
21+
}
22+

0 commit comments

Comments
 (0)