diff --git a/.gitignore b/.gitignore index dba32ad..d74d6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ node_modules/ # Java backend backend-java/build/ backend-java/.gradle/ + +# K8s secrets (never commit) +k8s/secrets.yaml diff --git a/backend-java/Dockerfile b/backend-java/Dockerfile new file mode 100644 index 0000000..c6bacfd --- /dev/null +++ b/backend-java/Dockerfile @@ -0,0 +1,16 @@ +# ── Build stage ── +FROM eclipse-temurin:21-jdk AS build +WORKDIR /app +COPY gradlew settings.gradle build.gradle ./ +COPY gradle/ gradle/ +RUN chmod +x gradlew && ./gradlew dependencies --no-daemon || true +COPY src/ src/ +RUN ./gradlew bootJar -x test --no-daemon + +# ── Runtime stage ── +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8000 +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC" +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/backend-java/src/main/java/com/tasteby/controller/VideoSseController.java b/backend-java/src/main/java/com/tasteby/controller/VideoSseController.java index 26af4c8..e3c4417 100644 --- a/backend-java/src/main/java/com/tasteby/controller/VideoSseController.java +++ b/backend-java/src/main/java/com/tasteby/controller/VideoSseController.java @@ -138,20 +138,31 @@ public class VideoSseController { } } - // Pass 2: retry missed + // Pass 2: retry missed (up to 3 attempts with smaller batches) if (!allMissed.isEmpty()) { emit(emitter, Map.of("type", "retry", "missed", allMissed.size())); - for (int i = 0; i < allMissed.size(); i += 10) { - var batch = allMissed.subList(i, Math.min(i + 10, allMissed.size())); - try { - var result = applyRemapBatch(batch); - updated += result.updated; - } catch (Exception ignored) {} + for (int attempt = 0; attempt < 3 && !allMissed.isEmpty(); attempt++) { + var retryList = new ArrayList<>(allMissed); + allMissed.clear(); + for (int i = 0; i < retryList.size(); i += 5) { + var batch = retryList.subList(i, Math.min(i + 5, retryList.size())); + try { + var result = applyRemapBatch(batch); + updated += result.updated; + allMissed.addAll(result.missed); + } catch (Exception e) { + log.warn("Remap cuisine retry failed (attempt {}): {}", attempt + 1, e.getMessage()); + allMissed.addAll(batch); + } + } + if (!allMissed.isEmpty()) { + emit(emitter, Map.of("type", "retry", "attempt", attempt + 2, "missed", allMissed.size())); + } } } cache.flush(); - emit(emitter, Map.of("type", "complete", "total", total, "updated", updated)); + emit(emitter, Map.of("type", "complete", "total", total, "updated", updated, "missed", allMissed.size())); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); @@ -172,7 +183,9 @@ public class VideoSseController { var rows = restaurantService.findForRemapFoods(); rows = rows.stream().map(r -> { var m = JsonUtil.lowerKeys(r); - m.put("foods", JsonUtil.parseStringList(m.get("foods_mentioned"))); + // foods_mentioned is now TO_CHAR'd in SQL, parse as string + Object fm = m.get("foods_mentioned"); + m.put("foods", JsonUtil.parseStringList(fm)); return m; }).toList(); @@ -191,23 +204,36 @@ public class VideoSseController { emit(emitter, Map.of("type", "batch_done", "current", Math.min(i + BATCH, total), "total", total, "updated", updated)); } catch (Exception e) { allMissed.addAll(batch); + log.warn("Remap foods batch error at {}: {}", i, e.getMessage()); emit(emitter, Map.of("type", "error", "message", e.getMessage(), "current", i)); } } + // Retry missed (up to 3 attempts with smaller batches) if (!allMissed.isEmpty()) { emit(emitter, Map.of("type", "retry", "missed", allMissed.size())); - for (int i = 0; i < allMissed.size(); i += 10) { - var batch = allMissed.subList(i, Math.min(i + 10, allMissed.size())); - try { - var r = applyFoodsBatch(batch); - updated += r.updated; - } catch (Exception ignored) {} + for (int attempt = 0; attempt < 3 && !allMissed.isEmpty(); attempt++) { + var retryList = new ArrayList<>(allMissed); + allMissed.clear(); + for (int i = 0; i < retryList.size(); i += 5) { + var batch = retryList.subList(i, Math.min(i + 5, retryList.size())); + try { + var r = applyFoodsBatch(batch); + updated += r.updated; + allMissed.addAll(r.missed); + } catch (Exception e) { + log.warn("Remap foods retry failed (attempt {}): {}", attempt + 1, e.getMessage()); + allMissed.addAll(batch); + } + } + if (!allMissed.isEmpty()) { + emit(emitter, Map.of("type", "retry", "attempt", attempt + 2, "missed", allMissed.size())); + } } } cache.flush(); - emit(emitter, Map.of("type", "complete", "total", total, "updated", updated)); + emit(emitter, Map.of("type", "complete", "total", total, "updated", updated, "missed", allMissed.size())); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); diff --git a/backend-java/src/main/java/com/tasteby/util/CuisineTypes.java b/backend-java/src/main/java/com/tasteby/util/CuisineTypes.java index c5ad168..97ec32f 100644 --- a/backend-java/src/main/java/com/tasteby/util/CuisineTypes.java +++ b/backend-java/src/main/java/com/tasteby/util/CuisineTypes.java @@ -15,12 +15,12 @@ public final class CuisineTypes { "한식|백반/한정식", "한식|국밥/해장국", "한식|찌개/전골/탕", "한식|삼겹살/돼지구이", "한식|소고기/한우구이", "한식|곱창/막창", "한식|닭/오리구이", "한식|족발/보쌈", "한식|회/횟집", "한식|해산물", "한식|분식", "한식|면", "한식|죽/죽집", - "한식|순대/순대국", "한식|장어/민물", "한식|주점/포차", + "한식|순대/순대국", "한식|장어/민물", "한식|주점/포차", "한식|파인다이닝/코스", "일식|스시/오마카세", "일식|라멘", "일식|돈카츠", "일식|텐동/튀김", - "일식|이자카야", "일식|야키니쿠", "일식|카레", "일식|소바/우동", - "중식|중화요리", "중식|마라/훠궈", "중식|딤섬/만두", "중식|양꼬치", + "일식|이자카야", "일식|야키니쿠", "일식|카레", "일식|소바/우동", "일식|파인다이닝/코스", + "중식|중화요리", "중식|마라/훠궈", "중식|딤섬/만두", "중식|양꼬치", "중식|파인다이닝/코스", "양식|파스타/이탈리안", "양식|스테이크", "양식|햄버거", "양식|피자", - "양식|프렌치", "양식|바베큐", "양식|브런치", "양식|비건/샐러드", + "양식|프렌치", "양식|바베큐", "양식|브런치", "양식|비건/샐러드", "양식|파인다이닝/코스", "아시아|베트남", "아시아|태국", "아시아|인도/중동", "아시아|동남아기타", "기타|치킨", "기타|카페/디저트", "기타|베이커리", "기타|뷔페", "기타|퓨전" ); diff --git a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml index 02d71c8..68e3e40 100644 --- a/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml +++ b/backend-java/src/main/resources/mybatis/mapper/RestaurantMapper.xml @@ -208,12 +208,13 @@ - UPDATE video_restaurants SET foods_mentioned = #{foods} WHERE id = #{id} + UPDATE video_restaurants SET foods_mentioned = #{foods,jdbcType=CLOB} WHERE id = #{id}