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 @@ -7,9 +7,11 @@
import com.onebridge.ouch.dto.hospital.response.AllDepartmentResponse;
import com.onebridge.ouch.dto.hospital.response.HospitalDetailResponse;
import com.onebridge.ouch.dto.hospital.response.HospitalDistanceResponse;
import com.onebridge.ouch.dto.hospital.response.RegionMappingDto;
import com.onebridge.ouch.service.department.DepartmentService;
import com.onebridge.ouch.service.hospital.HospitalDetailService;
import com.onebridge.ouch.service.hospital.HospitalSearchService;
import com.onebridge.ouch.service.hospital.RegionService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -23,6 +25,7 @@ public class HospitalController {
private final HospitalSearchService hospitalSearchService;
private final HospitalDetailService hospitalDetailService;
private final DepartmentService departmentService;
private final RegionService regionService;

@Operation(summary = "거리 순 병원 조회 API", description = "입력된 진료과(department), 종별코드명(type) 위도(lat), 경도(lng)를 기준으로 병원 목록을 거리 순으로 조회합니다. "
+ "진료과나 종별코드명를 입력하지 않으면 입력된 위도, 경도를 기준으로 모든 병원 목록을 거리 순으로 조회합니다. 위도, 경도는 필수로 입력해야 합니다. "
Expand All @@ -32,12 +35,13 @@ public class HospitalController {
public List<HospitalDistanceResponse> searchHospitals(
@RequestParam(required = false) String department,
@RequestParam(required = false) String type, // 종별코드명
@RequestParam(required = false) String sido,
@RequestParam Double lat,
@RequestParam Double lng,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return hospitalSearchService.searchHospitals(department, type, lat, lng, page, size);
return hospitalSearchService.searchHospitals(department, type, sido, lat, lng, page, size);
}

@Operation(summary = "병원 상세 조회 API", description = "입력된 병원 고유ID를 통해 병원 상세 정보를 조회합니다.")
Expand All @@ -51,4 +55,10 @@ public HospitalDetailResponse getHospitalDetail(@PathVariable String ykiho) {
public List<AllDepartmentResponse> getDepartments() {
return departmentService.getAllDepartments();
}

@Operation(summary = "시도(Region) 목록 조회", description = "한국어 시도명과 외국어 표준명을 모두 조회합니다.")
@GetMapping
public List<RegionMappingDto> getRegions() throws Exception {
return regionService.getAllRegions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.onebridge.ouch.dto.hospital.response;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class RegionMappingDto {
private String kr;
private String en;
private String zh;
}
206 changes: 187 additions & 19 deletions src/main/java/com/onebridge/ouch/realtime/RealtimeSessionController.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package com.onebridge.ouch.realtime;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

@RestController
public class RealtimeSessionController {

private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();

@Value("${openai.api-key}")
private String openaiApiKey;

Expand All @@ -18,20 +29,20 @@ public ResponseEntity<String> createEphemeralEnKey() {
headers.set("Authorization", "Bearer " + openaiApiKey);
headers.set("Content-Type", "application/json");

// String instructions = "너는 영어-한국어 번역기야. 오직 번역만 해. "
// + "영어를 들으면 한국어로, 한국어를 들으면 영어로 번역만 해. "
// + "어떤 말이든 개인적인 대답을 하지 말고 문장 그대로 번역만 해. ";

String instructions = "너는 영어-한국어 번역기야. 오직 번역만 해. "
+ "병원에 방문해서 대화에 사용되는 문장들이 입력될건데 어떤 말이든 너랑 대화하려는 거 아니니까 절대 개인적인 대답하지 말고 어떤 문장이든 영어는 한국어로, 한국어는 영어로 입력된 문장 그대로 번역만 해. "
+ "특히 질문이나 you, I를 포함한 문장도 그대로 번역만 해.";
// "너는 영어-한국어 번역기야. 오직 번역만 해. "
// + "병원에서 대화에 사용되는 문장들이 입력될건데 어떤 말이든 너랑 대화하려는 거 아니니까 절대 개인적인 대답하지 말고 어떤 문장이든 영어는 한국어로, 한국어는 영어로 입력된 문장 그대로 번역만 해. ";
//한국 병원에서 영어를 사용하는 환자와 한국어를 사용하는 병원 관계자의 대화가 입력될거야
String instructions = "영어를 사용하는 사람과 한국어를 사용하는 사람이 대화를 하는 상황이므로 영어와 한국어로 통역이 필요해. "
+ "따라서 한국 말은 영어로, 영어는 한국어로 통역을 해 줘. "
+ "너는 절대 개인적인 대답이나 조언을 하지 말고 번역만 진행하면 돼. "
+ "너한테 말걸어도 문장 그대로 번역만 해. 천천히 친절하게 대답해.";

// "You are a strict translation assistant specializing exclusively in medical scenarios for a Korean hospital setting. "
// + "- Your SOLE task is strict translation. Under NO circumstances should you answer questions, provide advice, or respond to any input other than translating. "
// + "- Korean input represents statements made by the doctor. Translate this into clear, polite, patient-friendly English using simple language, spoken slowly. "
// + "- English input represents statements made by the patient. Translate this into clear, polite, respectful Korean using simple expressions, spoken slowly to ensure comprehension. "
// + "- DO NOT engage in conversation, respond to direct questions, or acknowledge statements addressed to you. Your ONLY action must be to translate precisely. "
// + "- DO NOT rephrase extensively or summarize; translate accurately with gentle phrasing suitable for medical interactions. "
// + "- If the input text is already correctly translated, return it exactly as provided without changes. "
// + "- The output must ONLY be the translated text, delivered slowly, gently, and in a reassuring manner appropriate for patients. Absolutely NO other communication is permitted.";
// String instructions = "영어를 사용하는 사람과 한국어를 사용하는 사람이 대화를 하는 상황이므로 영어와 한국어로 통역이 필요해. "
// + "따라서 한국 말은 영어로, 영어는 한국어로 통역을 해 줘. "
// + "너는 절대 개인적인 대답이나 조언을 하지 말고 번역만 진행하면 돼. "
// + "너한테 말걸어도 문장 그대로 번역만 해. 천천히 친절하게 대답해.";

String requestBody = "{"
+ "\"model\": \"gpt-4o-mini-realtime-preview-2024-12-17\","
Expand All @@ -45,7 +56,8 @@ public ResponseEntity<String> createEphemeralEnKey() {
+ "\"input_audio_transcription\": {" //새로 나온 모델 추가
+ "\"model\": \"gpt-4o-mini-transcribe\","
// + "\"language\": \"\","
+ "\"prompt\": \"This audio input may contain both Korean and English words mixed together. Please transcribe both languages accurately.\""
+ "\"prompt\": \"한국어와 영어만 입력됩니다.\""
//+ "\"prompt\": \"This audio input may contain both Korean and English words mixed together. Please transcribe both languages accurately.\""
+ "},"

+ "\"turn_detection\": {"
Expand Down Expand Up @@ -88,10 +100,9 @@ public ResponseEntity<String> createEphemeralZhKey() {
headers.set("Content-Type", "application/json");

//한국 병원에서 영어를 사용하는 환자와 한국어를 사용하는 병원 관계자의 대화가 입력될거야
String instructions = "중국어를 사용하는 사람과 한국어를 사용하는 사람이 대화를 하는 상황이므로 중국어와 한국어로 통역이 필요해. "
+ "따라서 한국 말은 중국어로, 중국어는 한국어로 통역을 해 줘. "
+ "너는 절대 개인적인 대답이나 조언을 하지 말고 번역만 진행하면 돼. "
+ "너한테 말걸어도 문장 그대로 번역만 해. 천천히 친절하게 대답해.";
String instructions = "너는 중국어-한국어 번역기야. 오직 번역만 해. "
+ "병원에 방문해서 대화에 사용되는 문장들이 입력될건데 어떤 말이든 너랑 대화하려는 거 아니니까 절대 개인적인 대답하지 말고 어떤 문장이든 중국어는 한국어로, 한국어는 중국어로 입력된 문장 그대로 번역만 해. "
+ "특히 질문이나 you, I를 포함한 문장도 그대로 번역만 해.";

String requestBody = "{"
+ "\"model\": \"gpt-4o-mini-realtime-preview-2024-12-17\","
Expand All @@ -105,7 +116,7 @@ public ResponseEntity<String> createEphemeralZhKey() {
+ "\"input_audio_transcription\": {" //새로 나온 모델 추가
+ "\"model\": \"gpt-4o-mini-transcribe\","
// + "\"language\": \"\","
+ "\"prompt\": \"This audio input may contain both Korean and Chinese words mixed together. Please transcribe both languages accurately.\""
+ "\"prompt\": \"중국어와 한국어만 입력됩니다.\""
+ "},"

+ "\"turn_detection\": {"
Expand All @@ -126,5 +137,162 @@ public ResponseEntity<String> createEphemeralZhKey() {
);

return response;

}
@PostMapping("/summarize/en")
public ResponseEntity<Map<String, String>> summarizeConversation(
@RequestBody ConversationRequest requestBody
) {
try {
// 1) 원문 문자열 배열을 합쳐서 fullConversation 만들기
StringBuilder sb = new StringBuilder();
for (String orig : requestBody.getMessages()) {
sb.append("[원문] ").append(orig.trim()).append("\n");
}
String fullConversation = sb.toString();

// 2) OpenAI Chat Completions API 호출을 위한 메시지 구성
List<Map<String, String>> messages = new ArrayList<>();

// system 메시지: 요약 역할 지시
messages.add(Map.of(
"role", "system",
"content",
"병원에서 환자가 나눈 대화입니다. 진료 내용을 파악해서 100자 이내로 영어로 요약해주세요."
));
// user 메시지: 실제 대화 내용
messages.add(Map.of(
"role", "user",
"content", fullConversation
));

Map<String, Object> openAiRequest = new HashMap<>();
openAiRequest.put("model", "gpt-4o-mini"); // 필요에 따라 변경 가능
openAiRequest.put("messages", messages);
openAiRequest.put("temperature", 0.5);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(openaiApiKey);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(openAiRequest, headers);
ResponseEntity<String> aiResponse = restTemplate.postForEntity(
"https://api.openai.com/v1/chat/completions",
entity,
String.class
);

if (!aiResponse.getStatusCode().is2xxSuccessful()) {
return ResponseEntity
.status(aiResponse.getStatusCode())
.body(Map.of("summary", "요약 생성 중 오류가 발생했습니다."));
}

// 3) OpenAI 응답 JSON에서 summary 추출
Map<?, ?> responseMap = objectMapper.readValue(aiResponse.getBody(), Map.class);
List<?> choices = (List<?>) responseMap.get("choices");
if (choices == null || choices.isEmpty()) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("summary", "AI 응답 형식이 예상과 다릅니다."));
}
Map<?, ?> firstChoice = (Map<?, ?>) choices.get(0);
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
String summary = message.get("content").toString().trim();

// 4) 요약 결과 반환
return ResponseEntity.ok(Map.of("summary", summary));

} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("summary", "서버 내부 오류로 요약에 실패했습니다."));
}
}

@PostMapping("/summarize/zh")
public ResponseEntity<Map<String, String>> summarizeConversationZh(
@RequestBody ConversationRequest requestBody
) {
try {
// 1) 원문 문자열 배열을 합쳐서 fullConversation 만들기
StringBuilder sb = new StringBuilder();
for (String orig : requestBody.getMessages()) {
sb.append("[원문] ").append(orig.trim()).append("\n");
}
String fullConversation = sb.toString();

// 2) OpenAI Chat Completions API 호출을 위한 메시지 구성
List<Map<String, String>> messages = new ArrayList<>();

// system 메시지: 요약 역할 지시
messages.add(Map.of(
"role", "system",
"content",
"병원에서 환자가 나눈 대화입니다. 진료 내용을 파악해서 중국어로 100자 이내로 요약해주세요."
));
// user 메시지: 실제 대화 내용
messages.add(Map.of(
"role", "user",
"content", fullConversation
));

Map<String, Object> openAiRequest = new HashMap<>();
openAiRequest.put("model", "gpt-4o-mini"); // 필요에 따라 변경 가능
openAiRequest.put("messages", messages);
openAiRequest.put("temperature", 0.5);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(openaiApiKey);

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(openAiRequest, headers);
ResponseEntity<String> aiResponse = restTemplate.postForEntity(
"https://api.openai.com/v1/chat/completions",
entity,
String.class
);

if (!aiResponse.getStatusCode().is2xxSuccessful()) {
return ResponseEntity
.status(aiResponse.getStatusCode())
.body(Map.of("summary", "요약 생성 중 오류가 발생했습니다."));
}

// 3) OpenAI 응답 JSON에서 summary 추출
Map<?, ?> responseMap = objectMapper.readValue(aiResponse.getBody(), Map.class);
List<?> choices = (List<?>) responseMap.get("choices");
if (choices == null || choices.isEmpty()) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("summary", "AI 응답 형식이 예상과 다릅니다."));
}
Map<?, ?> firstChoice = (Map<?, ?>) choices.get(0);
Map<?, ?> message = (Map<?, ?>) firstChoice.get("message");
String summary = message.get("content").toString().trim();

// 4) 요약 결과 반환
return ResponseEntity.ok(Map.of("summary", summary));

} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("summary", "서버 내부 오류로 요약에 실패했습니다."));
}
}

/** 클라이언트가 보내는 JSON 구조를 매핑하기 위한 DTO */
public static class ConversationRequest {
@JsonProperty("messages")
private List<String> messages;

public List<String> getMessages() {
return messages;
}
public void setMessages(List<String> messages) {
this.messages = messages;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ List<Object[]> findAllOrderByDistance(
"AND h.type != '요양병원' " +
"AND (:department1 IS NULL OR hd.department_name IN (:department1, :department2)) " + // 내과+가정의학과 포함
"AND h.lat IS NOT NULL AND h.lng IS NOT NULL " +
"AND (:sidoKr IS NULL OR h.sido = :sidoKr) " +
"GROUP BY h.ykiho " +
"ORDER BY distance ASC " +
"LIMIT :limit OFFSET :offset",
Expand All @@ -59,6 +60,7 @@ List<Object[]> findWithConditionsOrderByDistance(
@Param("type") String type,
@Param("department1") String department1,
@Param("department2") String department2,
@Param("sidoKr") String sidoKr,
@Param("limit") int limit,
@Param("offset") int offset
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,22 @@
@RequiredArgsConstructor
public class HospitalSearchService {
private final HospitalRepository hospitalRepository;
private final RegionService regionService;

public List<HospitalDistanceResponse> searchHospitals(
String department,
String type, // 종별코드명 ex. '약국', '병원'
String sido,
Double lat,
Double lng,
int page,
int size
) {
if (lat == null || lng == null) throw new IllegalArgumentException("좌표(lat, lng)는 필수입니다.");

// 1) 전달받은 sido를 한국어로 변환
String sidoKr = regionService.getKrSidoName(sido);

int offset = page * size;
List<Object[]> rawList;

Expand All @@ -42,10 +47,10 @@ public List<HospitalDistanceResponse> searchHospitals(
}

// type(종별코드명)도 null/입력값에 따라 처리
if ((department1 != null && department2 != null) || (type != null && !type.isBlank())) {
if ((department1 != null && department2 != null) || (type != null && !type.isBlank()) || (sidoKr != null)) {
rawList = hospitalRepository.findWithConditionsOrderByDistance(
lat, lng,
type, department1, department2, size, offset
type, department1, department2, sidoKr, size, offset
);
} else {
rawList = hospitalRepository.findAllOrderByDistance(lat, lng, size, offset);
Expand Down
Loading