Files
tasteby/backend-java/src/main/java/com/tasteby/service/RestaurantService.java
joungmin cdee37e341 UI/UX 개선: 모바일 네비게이션, 로그인 모달, 지도 기능, 캐치테이블 연동
- 모바일 하단 네비게이션(홈/식당목록/내주변/찜/내정보) 추가
- 로그인 버튼을 모달 방식으로 변경 (소셜 로그인 확장 가능)
- 내위치 기반 정렬 및 영역 필터, 지도 내위치 버튼 추가
- 채널 필터 시 해당 채널만 마커/범례 표시
- 캐치테이블 검색/연동 (단건/벌크), NONE 저장 패턴
- 벌크 트랜스크립트 SSE (Playwright 브라우저 재사용)
- 테이블링/캐치테이블 버튼 UI 차별화
- Google Maps 링크 모바일 호환, 초기화 버튼, 검색 라벨 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:49:16 +09:00

171 lines
6.5 KiB
Java

package com.tasteby.service;
import com.tasteby.domain.Restaurant;
import com.tasteby.mapper.RestaurantMapper;
import com.tasteby.util.IdGenerator;
import com.tasteby.util.JsonUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class RestaurantService {
private final RestaurantMapper mapper;
public RestaurantService(RestaurantMapper mapper) {
this.mapper = mapper;
}
public List<Restaurant> findAll(int limit, int offset, String cuisine, String region, String channel) {
List<Restaurant> restaurants = mapper.findAll(limit, offset, cuisine, region, channel);
enrichRestaurants(restaurants);
return restaurants;
}
public List<Restaurant> findWithoutTabling() {
return mapper.findWithoutTabling();
}
public List<Restaurant> findWithoutCatchtable() {
return mapper.findWithoutCatchtable();
}
public Restaurant findById(String id) {
Restaurant restaurant = mapper.findById(id);
if (restaurant == null) return null;
enrichRestaurants(List.of(restaurant));
return restaurant;
}
public List<Map<String, Object>> findVideoLinks(String restaurantId) {
var rows = mapper.findVideoLinks(restaurantId);
return rows.stream().map(row -> {
var m = JsonUtil.lowerKeys(row);
m.put("foods_mentioned", JsonUtil.parseStringList(m.get("foods_mentioned")));
m.put("evaluation", JsonUtil.parseMap(m.get("evaluation")));
m.put("guests", JsonUtil.parseStringList(m.get("guests")));
return m;
}).toList();
}
public void update(String id, Map<String, Object> fields) {
mapper.updateFields(id, fields);
}
@Transactional
public void delete(String id) {
mapper.deleteVectors(id);
mapper.deleteReviews(id);
mapper.deleteFavorites(id);
mapper.deleteVideoRestaurants(id);
mapper.deleteRestaurant(id);
}
public String upsert(Map<String, Object> data) {
String placeId = (String) data.get("google_place_id");
String existingId = null;
if (placeId != null && !placeId.isBlank()) {
existingId = mapper.findIdByPlaceId(placeId);
}
if (existingId == null) {
existingId = mapper.findIdByName((String) data.get("name"));
}
Restaurant r = Restaurant.builder()
.name(truncateBytes((String) data.get("name"), 200))
.address(truncateBytes((String) data.get("address"), 500))
.region((String) data.get("region"))
.latitude(data.get("latitude") instanceof Number n ? n.doubleValue() : null)
.longitude(data.get("longitude") instanceof Number n ? n.doubleValue() : null)
.cuisineType((String) data.get("cuisine_type"))
.priceRange((String) data.get("price_range"))
.googlePlaceId(placeId)
.phone((String) data.get("phone"))
.website((String) data.get("website"))
.businessStatus((String) data.get("business_status"))
.rating(data.get("rating") instanceof Number n ? n.doubleValue() : null)
.ratingCount(data.get("rating_count") instanceof Number n ? n.intValue() : null)
.build();
if (existingId != null) {
r.setId(existingId);
mapper.updateRestaurant(r);
return existingId;
} else {
String newId = IdGenerator.newId();
r.setId(newId);
mapper.insertRestaurant(r);
return newId;
}
}
public void linkVideoRestaurant(String videoId, String restaurantId, List<String> foods, String evaluation, List<String> guests) {
String id = IdGenerator.newId();
String foodsJson = foods != null ? JsonUtil.toJson(foods) : null;
String guestsJson = guests != null ? JsonUtil.toJson(guests) : null;
mapper.linkVideoRestaurant(id, videoId, restaurantId, foodsJson, evaluation, guestsJson);
}
public void updateCuisineType(String id, String cuisineType) {
mapper.updateCuisineType(id, cuisineType);
}
public void updateFoodsMentioned(String id, String foods) {
mapper.updateFoodsMentioned(id, foods);
}
public List<Map<String, Object>> findForRemapCuisine() {
return mapper.findForRemapCuisine();
}
public List<Map<String, Object>> findForRemapFoods() {
return mapper.findForRemapFoods();
}
private void enrichRestaurants(List<Restaurant> restaurants) {
if (restaurants.isEmpty()) return;
List<String> ids = restaurants.stream().map(Restaurant::getId).filter(Objects::nonNull).toList();
if (ids.isEmpty()) return;
// Channels
List<Map<String, Object>> channelRows = mapper.findChannelsByRestaurantIds(ids);
Map<String, List<String>> channelMap = new HashMap<>();
for (var row : channelRows) {
String rid = (String) row.getOrDefault("restaurant_id", row.get("RESTAURANT_ID"));
String ch = (String) row.getOrDefault("channel_name", row.get("CHANNEL_NAME"));
if (rid != null && ch != null) {
channelMap.computeIfAbsent(rid, k -> new ArrayList<>()).add(ch);
}
}
// Foods
List<Map<String, Object>> foodRows = mapper.findFoodsByRestaurantIds(ids);
Map<String, Set<String>> foodMap = new HashMap<>();
for (var row : foodRows) {
String rid = (String) row.getOrDefault("restaurant_id", row.get("RESTAURANT_ID"));
Object foods = row.getOrDefault("foods_mentioned", row.get("FOODS_MENTIONED"));
if (rid != null && foods != null) {
List<String> parsed = JsonUtil.parseStringList(foods);
foodMap.computeIfAbsent(rid, k -> new LinkedHashSet<>()).addAll(parsed);
}
}
for (var r : restaurants) {
r.setChannels(channelMap.getOrDefault(r.getId(), List.of()));
Set<String> foods = foodMap.get(r.getId());
r.setFoodsMentioned(foods != null ? new ArrayList<>(foods) : List.of());
}
}
private String truncateBytes(String s, int maxBytes) {
if (s == null) return null;
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
if (bytes.length <= maxBytes) return s;
return new String(bytes, 0, maxBytes, StandardCharsets.UTF_8);
}
}