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}