diff --git a/backend-java/src/main/java/com/tasteby/config/JdbcConfig.java b/backend-java/src/main/java/com/tasteby/config/JdbcConfig.java new file mode 100644 index 0000000..7b365ec --- /dev/null +++ b/backend-java/src/main/java/com/tasteby/config/JdbcConfig.java @@ -0,0 +1,53 @@ +package com.tasteby.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * Custom JdbcTemplate that returns lowercase column names. + * Oracle returns UPPERCASE column names by default; this normalizes them + * so the JSON API returns snake_case keys matching the frontend expectations. + */ +@Configuration +public class JdbcConfig { + + @Bean + @Primary + public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) { + JdbcTemplate jdbc = new LowerCaseJdbcTemplate(dataSource); + return new NamedParameterJdbcTemplate(jdbc); + } + + @Bean + @Primary + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new LowerCaseJdbcTemplate(dataSource); + } + + /** + * JdbcTemplate subclass that uses a lowercase ColumnMapRowMapper. + */ + static class LowerCaseJdbcTemplate extends JdbcTemplate { + LowerCaseJdbcTemplate(DataSource dataSource) { + super(dataSource); + } + + @Override + protected RowMapper> getColumnMapRowMapper() { + return new ColumnMapRowMapper() { + @Override + protected String getColumnKey(String columnName) { + return columnName.toLowerCase(); + } + }; + } + } +} diff --git a/backend-java/src/main/java/com/tasteby/config/LowerCaseKeyAdvice.java b/backend-java/src/main/java/com/tasteby/config/LowerCaseKeyAdvice.java new file mode 100644 index 0000000..1a81260 --- /dev/null +++ b/backend-java/src/main/java/com/tasteby/config/LowerCaseKeyAdvice.java @@ -0,0 +1,46 @@ +package com.tasteby.config; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.*; + +/** + * Automatically converts Oracle UPPERCASE map keys to lowercase in all API responses. + */ +@RestControllerAdvice(basePackages = "com.tasteby") +public class LowerCaseKeyAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + return convertKeys(body); + } + + @SuppressWarnings("unchecked") + private Object convertKeys(Object obj) { + if (obj instanceof Map map) { + var result = new LinkedHashMap(); + for (var entry : map.entrySet()) { + String key = entry.getKey() instanceof String s ? s.toLowerCase() : String.valueOf(entry.getKey()); + result.put(key, convertKeys(entry.getValue())); + } + return result; + } + if (obj instanceof List list) { + return list.stream().map(this::convertKeys).toList(); + } + return obj; + } +} diff --git a/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java b/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java index d25bc97..450db12 100644 --- a/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java +++ b/backend-java/src/main/java/com/tasteby/controller/AdminUserController.java @@ -54,8 +54,8 @@ public class AdminUserController { """; var rows = jdbc.queryForList(sql, new MapSqlParameterSource("u", userId)); rows.forEach(r -> { - Object text = r.get("REVIEW_TEXT"); - r.put("REVIEW_TEXT", JsonUtil.readClob(text)); + Object text = r.get("review_text"); + r.put("review_text", JsonUtil.readClob(text)); }); return rows; } diff --git a/backend-java/src/main/java/com/tasteby/controller/DaemonController.java b/backend-java/src/main/java/com/tasteby/controller/DaemonController.java index c39edc1..e818adc 100644 --- a/backend-java/src/main/java/com/tasteby/controller/DaemonController.java +++ b/backend-java/src/main/java/com/tasteby/controller/DaemonController.java @@ -30,14 +30,14 @@ public class DaemonController { if (rows.isEmpty()) return Map.of(); var row = rows.getFirst(); var result = new LinkedHashMap(); - result.put("scan_enabled", toInt(row.get("SCAN_ENABLED")) == 1); - result.put("scan_interval_min", row.get("SCAN_INTERVAL_MIN")); - result.put("process_enabled", toInt(row.get("PROCESS_ENABLED")) == 1); - result.put("process_interval_min", row.get("PROCESS_INTERVAL_MIN")); - result.put("process_limit", row.get("PROCESS_LIMIT")); - result.put("last_scan_at", row.get("LAST_SCAN_AT") != null ? row.get("LAST_SCAN_AT").toString() : null); - result.put("last_process_at", row.get("LAST_PROCESS_AT") != null ? row.get("LAST_PROCESS_AT").toString() : null); - result.put("updated_at", row.get("UPDATED_AT") != null ? row.get("UPDATED_AT").toString() : null); + result.put("scan_enabled", toInt(row.get("scan_enabled")) == 1); + result.put("scan_interval_min", row.get("scan_interval_min")); + result.put("process_enabled", toInt(row.get("process_enabled")) == 1); + result.put("process_interval_min", row.get("process_interval_min")); + result.put("process_limit", row.get("process_limit")); + result.put("last_scan_at", row.get("last_scan_at") != null ? row.get("last_scan_at").toString() : null); + result.put("last_process_at", row.get("last_process_at") != null ? row.get("last_process_at").toString() : null); + result.put("updated_at", row.get("updated_at") != null ? row.get("updated_at").toString() : null); return result; } diff --git a/backend-java/src/main/java/com/tasteby/repository/RestaurantRepository.java b/backend-java/src/main/java/com/tasteby/repository/RestaurantRepository.java index ccdeb32..5b8aeac 100644 --- a/backend-java/src/main/java/com/tasteby/repository/RestaurantRepository.java +++ b/backend-java/src/main/java/com/tasteby/repository/RestaurantRepository.java @@ -230,14 +230,14 @@ public class RestaurantRepository { var rows = namedJdbc.queryForList( "SELECT id FROM restaurants WHERE google_place_id = :gid", new MapSqlParameterSource("gid", placeId)); - return rows.isEmpty() ? null : (String) rows.getFirst().get("ID"); + return rows.isEmpty() ? null : (String) rows.getFirst().get("id"); } private String findIdByName(String name) { var rows = namedJdbc.queryForList( "SELECT id FROM restaurants WHERE name = :n", new MapSqlParameterSource("n", name)); - return rows.isEmpty() ? null : (String) rows.getFirst().get("ID"); + return rows.isEmpty() ? null : (String) rows.getFirst().get("id"); } private MapSqlParameterSource buildUpsertParams(Map data, @@ -267,7 +267,7 @@ public class RestaurantRepository { } private void attachChannels(List> rows) { - List ids = rows.stream().map(r -> (String) r.get("ID")).filter(Objects::nonNull).toList(); + List ids = rows.stream().map(r -> (String) r.get("id")).filter(Objects::nonNull).toList(); if (ids.isEmpty()) return; var params = new MapSqlParameterSource(); @@ -290,13 +290,13 @@ public class RestaurantRepository { .add(rs.getString("CHANNEL_NAME")); }); for (var r : rows) { - String id = (String) r.get("ID"); + String id = (String) r.get("id"); r.put("channels", chMap.getOrDefault(id, List.of())); } } private void attachFoodsMentioned(List> rows) { - List ids = rows.stream().map(r -> (String) r.get("ID")).filter(Objects::nonNull).toList(); + List ids = rows.stream().map(r -> (String) r.get("id")).filter(Objects::nonNull).toList(); if (ids.isEmpty()) return; var params = new MapSqlParameterSource(); @@ -323,7 +323,7 @@ public class RestaurantRepository { } }); for (var r : rows) { - String id = (String) r.get("ID"); + String id = (String) r.get("id"); List all = foodsMap.getOrDefault(id, List.of()); r.put("foods_mentioned", all.size() > 10 ? all.subList(0, 10) : all); } diff --git a/backend-java/src/main/java/com/tasteby/repository/ReviewRepository.java b/backend-java/src/main/java/com/tasteby/repository/ReviewRepository.java index a11da3b..aa28bfb 100644 --- a/backend-java/src/main/java/com/tasteby/repository/ReviewRepository.java +++ b/backend-java/src/main/java/com/tasteby/repository/ReviewRepository.java @@ -101,8 +101,8 @@ public class ReviewRepository { """; var row = jdbc.queryForMap(sql, new MapSqlParameterSource("rid", restaurantId)); return Map.of( - "avg_rating", row.get("AVG_RATING") != null ? ((Number) row.get("AVG_RATING")).doubleValue() : null, - "review_count", ((Number) row.get("REVIEW_COUNT")).intValue() + "avg_rating", row.get("avg_rating") != null ? ((Number) row.get("avg_rating")).doubleValue() : null, + "review_count", ((Number) row.get("review_count")).intValue() ); } diff --git a/backend-java/src/main/java/com/tasteby/repository/UserRepository.java b/backend-java/src/main/java/com/tasteby/repository/UserRepository.java index 9fe284d..c63e2ce 100644 --- a/backend-java/src/main/java/com/tasteby/repository/UserRepository.java +++ b/backend-java/src/main/java/com/tasteby/repository/UserRepository.java @@ -31,15 +31,15 @@ public class UserRepository { if (!rows.isEmpty()) { // Update last_login_at var row = rows.getFirst(); - String userId = (String) row.get("ID"); + String userId = (String) row.get("id"); jdbc.update("UPDATE tasteby_users SET last_login_at = SYSTIMESTAMP WHERE id = :id", new MapSqlParameterSource("id", userId)); return Map.of( "id", userId, - "email", row.getOrDefault("EMAIL", ""), - "nickname", row.getOrDefault("NICKNAME", ""), - "avatar_url", row.getOrDefault("AVATAR_URL", ""), - "is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1 + "email", row.getOrDefault("email", ""), + "nickname", row.getOrDefault("nickname", ""), + "avatar_url", row.getOrDefault("avatar_url", ""), + "is_admin", row.get("is_admin") != null && ((Number) row.get("is_admin")).intValue() == 1 ); } @@ -70,11 +70,11 @@ public class UserRepository { if (rows.isEmpty()) return null; var row = rows.getFirst(); return Map.of( - "id", row.get("ID"), - "email", row.getOrDefault("EMAIL", ""), - "nickname", row.getOrDefault("NICKNAME", ""), - "avatar_url", row.getOrDefault("AVATAR_URL", ""), - "is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1 + "id", row.get("id"), + "email", row.getOrDefault("email", ""), + "nickname", row.getOrDefault("nickname", ""), + "avatar_url", row.getOrDefault("avatar_url", ""), + "is_admin", row.get("is_admin") != null && ((Number) row.get("is_admin")).intValue() == 1 ); } diff --git a/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java b/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java index aa6e561..8890e8a 100644 --- a/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java +++ b/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java @@ -84,13 +84,13 @@ public class DaemonScheduler { if (rows.isEmpty()) return null; var row = rows.getFirst(); return new DaemonConfig( - toInt(row.get("SCAN_ENABLED")) == 1, - toInt(row.get("SCAN_INTERVAL_MIN")), - toInt(row.get("PROCESS_ENABLED")) == 1, - toInt(row.get("PROCESS_INTERVAL_MIN")), - toInt(row.get("PROCESS_LIMIT")), - row.get("LAST_SCAN_AT") instanceof java.sql.Timestamp ts ? ts.toInstant() : null, - row.get("LAST_PROCESS_AT") instanceof java.sql.Timestamp ts ? ts.toInstant() : null + toInt(row.get("scan_enabled")) == 1, + toInt(row.get("scan_interval_min")), + toInt(row.get("process_enabled")) == 1, + toInt(row.get("process_interval_min")), + toInt(row.get("process_limit")), + row.get("last_scan_at") instanceof java.sql.Timestamp ts ? ts.toInstant() : null, + row.get("last_process_at") instanceof java.sql.Timestamp ts ? ts.toInstant() : null ); } catch (Exception e) { log.debug("Cannot read daemon config: {}", e.getMessage()); diff --git a/backend-java/src/main/java/com/tasteby/service/SearchService.java b/backend-java/src/main/java/com/tasteby/service/SearchService.java index 5d0bce1..b246f37 100644 --- a/backend-java/src/main/java/com/tasteby/service/SearchService.java +++ b/backend-java/src/main/java/com/tasteby/service/SearchService.java @@ -49,13 +49,11 @@ public class SearchService { Set seen = new HashSet<>(); var merged = new ArrayList>(); for (var r : kw) { - String id = (String) r.get("ID"); - if (id == null) id = (String) r.get("id"); + String id = (String) r.get("id"); if (seen.add(id)) merged.add(r); } for (var r : sem) { - String id = (String) r.get("ID"); - if (id == null) id = (String) r.get("id"); + String id = (String) r.get("id"); if (seen.add(id)) merged.add(r); } result = merged.size() > limit ? merged.subList(0, limit) : merged; @@ -123,11 +121,7 @@ public class SearchService { private void attachChannels(List> rows) { List ids = rows.stream() - .map(r -> { - Object id = r.get("ID"); - if (id == null) id = r.get("id"); - return (String) id; - }) + .map(r -> (String) r.get("id")) .filter(Objects::nonNull).toList(); if (ids.isEmpty()) return; @@ -151,8 +145,7 @@ public class SearchService { .add(rs.getString("CHANNEL_NAME")); }); for (var r : rows) { - String id = (String) r.get("ID"); - if (id == null) id = (String) r.get("id"); + String id = (String) r.get("id"); r.put("channels", chMap.getOrDefault(id, List.of())); } } diff --git a/backend-java/src/main/java/com/tasteby/service/YouTubeService.java b/backend-java/src/main/java/com/tasteby/service/YouTubeService.java index 49bb450..594ef28 100644 --- a/backend-java/src/main/java/com/tasteby/service/YouTubeService.java +++ b/backend-java/src/main/java/com/tasteby/service/YouTubeService.java @@ -154,8 +154,8 @@ public class YouTubeService { var ch = channelRepo.findByChannelId(channelId); if (ch == null) return null; - String dbId = (String) ch.get("ID"); - String titleFilter = (String) ch.get("TITLE_FILTER"); + String dbId = (String) ch.get("id"); + String titleFilter = (String) ch.get("title_filter"); String after = full ? null : videoRepo.getLatestVideoDate(dbId); Set existing = videoRepo.getExistingVideoIds(dbId); diff --git a/backend-java/src/main/java/com/tasteby/util/JsonUtil.java b/backend-java/src/main/java/com/tasteby/util/JsonUtil.java index 1d0b996..373afb8 100644 --- a/backend-java/src/main/java/com/tasteby/util/JsonUtil.java +++ b/backend-java/src/main/java/com/tasteby/util/JsonUtil.java @@ -6,9 +6,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.Reader; import java.sql.Clob; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** * JSON utility for handling Oracle CLOB JSON fields. @@ -58,6 +57,23 @@ public final class JsonUtil { } } + /** + * Convert Oracle uppercase column keys to lowercase (e.g. "ID" → "id", "CUISINE_TYPE" → "cuisine_type"). + */ + public static Map lowerKeys(Map row) { + if (row == null) return null; + var result = new LinkedHashMap(row.size()); + for (var entry : row.entrySet()) { + result.put(entry.getKey().toLowerCase(), entry.getValue()); + } + return result; + } + + public static List> lowerKeys(List> rows) { + if (rows == null) return null; + return rows.stream().map(JsonUtil::lowerKeys).collect(Collectors.toList()); + } + public static String toJson(Object value) { try { return MAPPER.writeValueAsString(value);