- νλ‘μ νΈ λͺ : IncluKiosk
- νλ‘μ νΈ μ μ: AI κΈ°μ (μ»΄ν¨ν° λΉμ , μμ μΆμ , LLM)μ νμ©νμ¬ λͺ¨λ μ¬μ©μλ₯Ό μν λ§μΆ€ν λ©ν°λͺ¨λ¬ μΈν°νμ΄μ€λ₯Ό μ 곡νλ ν¬μ©μ (Inclusive) ν€μ€μ€ν¬ μμ€ν
- λ¬Έμ μΈμ: ν€μ€μ€ν¬ 보νΈνμ λ°λ₯Έ λμ§νΈ 격차 μ¬ν
- κ³ μ λ νλ©΄ λμ΄μ ν°μΉ μ€μ¬μ λ¨μΌ λ°©μμΌλ‘ μΈν΄ νΉμ μ¬μ©μμΈ΅(ν μ²΄μ΄ μ¬μ©μ, λ ΈμΈ, μκ°μ₯μ μΈ λ±)μ μ΄μ©μ΄ μ΄λ €μ
- μ¬νμ μꡬ: ν¬μ©μ κΈ°μ κ³Ό 보νΈμ λμμΈμ λν μ¬νμ νμμ± μ¦λ
- νλ‘μ νΈ λͺ©ν: λͺ¨λ μ¬μ©μκ° μ°¨λ³ μμ΄ μλΉμ€λ₯Ό μ΄μ©νλ νκ²½μ μ‘°μ±νμ¬ λμ§νΈ μμΈ λ¬Έμ ν΄κ²°
- μ¬μ©μ μλ μΈμ λ° νλ©΄ μ΅μ ν
- μ»΄ν¨ν° λΉμ μΌλ‘ μ¬μ©μ μ μ₯μ μΈμ, μ΅μ μ λμ΄λ‘ μλ μ‘°μ
- μ¬μ©μ μν©μ λ§λ λ§μΆ€ν UI μ 곡
- λ€μ€ μ
λ ₯ λ°©μμ μ§μνλ λ©ν°λͺ¨λ¬ μΈν°νμ΄μ€
- κΈ°λ³Έ ν°μΉ λ°©μμ μμ μΆμ (Eye-tracking) λ° μμ± μΈμ κΈ°λ₯ ν΅ν©
- μ¬μ©μκ° μμ μκ² κ°μ₯ νΈλ¦¬ν μ λ ₯ λ°©μμ μ ν κ°λ₯
- LLM κΈ°λ° μ§λ₯ν λν μμ€ν
- λ¨μ λͺ λ Ήμ΄λ₯Ό λμ΄, λ¬Έλ§₯μ μ΄ν΄νλ μμ°μ΄ μ²λ¦¬ λ₯λ ₯ ν보
- λ©λ΄ μΆμ², νΉμ μ±λΆ λ¬Έμ λ± λ³΅ν©μ μΈ μ§λ¬Έμ λν λνν μλ κ°λ₯
- μ μν νλ©΄ λμ΄ μ‘°μ : AI κΈ°λ° μ¬μ©μ μΈμ λ° νλ©΄ λμ΄ μλ μ‘°μ
- μμ΄νΈλνΉ μΈν°νμ΄μ€: μμ μμ§μμ ν΅ν λ©λ΄ μ ν λ° μ μ΄
- μμ± μ±λ΄: LLM κΈ°λ° μμ± μ£Όλ¬Έ λ° λνν μ§μμλ΅
- λ€κ΅μ΄ μ§μ: νκ΅μ΄, μμ΄, μ€κ΅μ΄ λ± λ€κ΅μ΄ μΈν°νμ΄μ€ μ 곡
- ν΅ν© μν€ν μ²: RESTful API κΈ°λ°μ μΌκ΄λ μ¬μ©μ κ²½ν
-
κΈ°λ ν¨κ³Ό
- κΈ°μ μ μΈ‘λ©΄: λ©ν°λͺ¨λ¬ AI μΈν°νμ΄μ€λ‘ κ³ λνλ μ¬μ©μ κ²½ν μ 곡
- μ¬νμ μΈ‘λ©΄: λμ§νΈ μ½μμ μ 보 μ κ·Όμ± λ° μ립λ ν₯μμ ν΅ν λμ§νΈ ν¬μ©μ± μ€ν
- μμ₯μ±: κ³ λ Ήν λ° μ₯μ μΈ κΆμ΅ νλμ λ°λ₯Έ μ κ·Όμ± μ€μ¬ ν€μ€μ€ν¬ μμ μΆ©μ‘±
-
νμ© λΆμΌ
- 곡곡기κ΄: λ―Όμ μ μ λ° μλ΄ μμ€ν
- μλ£κΈ°κ΄: μ μ, μλ©, μλ΄ μμ€ν
- κ΅ν΅μμ€: λ°κΆ λ° λ€κ΅μ΄ μλ΄ μμ€ν (곡ν, ν°λ―Έλ λ±)
- μμ μμ€: μ£Όλ¬Έ μμ€ν (μΉ΄ν, μλΉ λ±)
- κ΅μ‘Β·λ¬Ένμμ€: μλ΄ λ° μμ½ μμ€ν (λμκ΄, λ°λ¬Όκ΄ λ±)
| κ΅¬λΆ | κΈ°μ |
|---|---|
| FE | React.js, Next.js, TypeScript, react-query, zustand, Electron, |
| BE | Java, Spring Boot, Gemini API |
| AI/ML | MediaPipe, OpenCV, PyCoral, TensorFlowLite, MobileNet-V2 |
| HW (IoT) | Python, WebSockets |
| DB | MongoDB, MySQL |
| Cloud | AWS (EC2 Β· S3 Β· CloudFront Β· Route53) |
| HW | λΌμ¦λ² 리νμ΄ 4 Model B (8GB RAM), λΌμ¦λ² 리νμ΄ μΉ΄λ©λΌλͺ¨λ V2, Seeed ReSpeaker Mic Array, 15.6μΈμΉ μ μ μ ν°μΉ λμ€νλ μ΄, 리λμ΄ μ‘μΆμμ΄ν° λ° TB6600 λͺ¨ν° λλΌμ΄λ² |
| μ νμΈ (@jho7535) | κ°μμ‘ (@kangeunsong) | κΉλν (@kdhqwe1030) | κΉλμ |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| β’ λ°±μλ κ°λ° β’ μλ² κ΄λ¦¬ |
β’ νλμ¨μ΄ μ μ΄ β’ AI κΈ°λ₯ κ°λ° |
β’ νλ‘ νΈμλ κ°λ° β’ UI/UX μ€κ³ |
β’ νλ‘μ νΈ λ©ν β’ κΈ°μ μλ¬Έ |
- λΌμ¦λ² 리νμ΄μμ WebSocket ν΅μ μ ν΅ν΄ STT/TTS νλ¦μ μ μ΄νκ³ , λ°±μλ REST API(ChatAPI) λ₯Ό ν΅ν΄ μ±λ΄ λνλ₯Ό μ²λ¦¬νλ ν΅μ¬ λ‘μ§μ λλ€.
// [ν΅μ¬ ν¨μ] Chat.tsx
// - sendMessage(): νλ‘ νΈ β λΌμ¦λ² 리νμ΄ λͺ
λ Ή μ μ‘
// - chatAPI.sendChat(): νλ‘ νΈ β λ°±μλ μ±λ΄ λν μμ²
// - case ꡬ문: λΌμ¦λ² 리νμ΄ β νλ‘ νΈλ‘ μμ λλ λ©μμ§ μ μ΄
useEffect(() => {
if (!isConnected) return;
const handle = async (msg: SocketMessage) => {
switch (msg.type) {
// 1οΈβ£ μλ΄ μμ± μ’
λ£ β STT μμ (λΌμ¦λ² 리νμ΄λ‘λΆν° μμ )
case "END_GUIDE":
sendMessage({ type: "STT_ON" }); // λΌμ¦λ² 리νμ΄μ μμ±μΈμ μμ λͺ
λ Ή
setIsListening(true);
break;
// 2οΈβ£ μμ± μΈμ μλ£(STT_OFF) β λ°±μλλ‘ μ¬μ©μ λ°ν μ λ¬
case "STT_OFF":
setChatLogs(prev => [β¦prev, { message: msg.message, isBot: false }]);
const res = await chatAPI.sendChat(shopId, {
sessionId,
message: msg.message,
storeId: Number(shopId),
storeName: shopName,
});
const answer = res?.aiMessage || "μ£μ‘ν©λλ€, λ΅λ³μ λΆλ¬μ€μ§ λͺ»νμ΅λλ€.";
setChatLogs(prev => [β¦prev, { message: answer, isBot: true }]);
// μ±λ΄ μλ΅μ λΌμ¦λ² 리νμ΄μ μ λ¬ β μμ± μΆλ ₯(TTS)
sendMessage({ type: "TTS_ON", message: answer });
break;
// 3οΈβ£ μμ± μΆλ ₯ μ’
λ£(TTS_OFF) β λ€μ λ°ν λκΈ°
case "TTS_OFF":
sendMessage({ type: "STT_ON" }); // λ€μ μμ±μΈμ μμ
setIsListening(true);
break;
}
};
addOnMessage(handle);
return () => removeOnMessage(handle);
}, [isConnected]);- EdgeTPU νλμ¨μ΄ κ°μκ³Ό 2λ¨κ³ ν΄λ°± μ λ΅(μΌκ΅΄ β μ¬λ)μ ν΅ν΄ μ¬μ©μ μμΉλ₯Ό μ€μκ°μΌλ‘ μΆμ νκ³ , EMA νν°λ§μΌλ‘ λ Έμ΄μ¦λ₯Ό μ κ±°νμ¬ λ¦¬λμ΄ μ‘μΆμμ΄ν° μ μ΄ ν¨μ(5-3)λ₯Ό νΈμΆνλ ν΅μ¬ λ‘μ§μ λλ€.
# height_worker.py - EdgeTPU κ°μ κ°μ²΄ κ°μ§
# 1οΈβ£ EdgeTPU λͺ¨λΈ λ‘λ (νλμ¨μ΄ κ°μ)
face_interpreter = tflite.Interpreter(
model_path=FACE_MODEL,
experimental_delegates=[tflite.load_delegate('libedgetpu.so.1')]
)
person_interpreter = tflite.Interpreter(
model_path=PERSON_MODEL,
experimental_delegates=[tflite.load_delegate('libedgetpu.so.1')]
)
# 2οΈβ£ 2λ¨κ³ ν΄λ°± μ λ΅
def track_height():
# μ°μ μμ 1: μΌκ΅΄ κ°μ§ (μ λ° μ μ΄)
faces = detect_faces_ssd(face_interpreter, frame, MIN_DET_CONF)
if faces:
# μΌκ΅΄ μ€μ¬μ νλ©΄ μ€μμΌλ‘
y_center = (ymin + ymax) * 0.5
ema_y = EMA_ALPHA * y_center + (1-EMA_ALPHA) * ema_y
diff = ema_y - target_y
if abs(diff) <= deadband:
state = "center" # β
μμ ν
elif diff < 0:
moveUp(WITH_FACE) # λΉ λ₯΄κ² μ‘°μ
else:
moveDown(WITH_FACE)
else:
# μ°μ μμ 2: μ¬λ μ 체 κ°μ§ (λλ΅μ μμΉ)
person = detect_person_ssd(person_interpreter, frame)
if person:
if person[1] <= 0.05: # νλ©΄ μλ¨
moveUp(WITHOUT_FACE) # μ²μ²ν μ‘°μ
elif person[1] >= 0.95: # νλ©΄ νλ¨
moveDown(WITHOUT_FACE)
# 3οΈβ£ EMA νν°λ‘ λ
Έμ΄μ¦ μ κ±°
ema_y = 0.3 * new_value + 0.7 * ema_y # λΆλλ¬μ΄ μμ§μ- AI λΉμ μμ€ν (5-2)μ νλ¨μ λ°λΌ GPIO νμ€ μ μ΄λ₯Ό ν΅ν΄ μ€ν λͺ¨ν°λ₯Ό μ λ°νκ² κ΅¬λνκ³ , λ€μ€ νκ³ κ²μ¦κ³Ό μ€μκ° λμ΄ μ μ₯μ ν΅ν΄ νλμ¨μ΄ μμ μ±μ 보μ₯νλ©°, νλ‘κ·Έλ¨ μ’ λ£ μ μλ μμ 볡κ·λ₯Ό μννλ ν΅μ¬ λ‘μ§μ λλ€.
# linear_actuator_controller.py - μ‘μΆμμ΄ν° μμ μ μ΄
# 1οΈβ£ νκ³ κ²μ¦ (νμΌ κΈ°λ° μν κ΄λ¦¬)
def exceed_max_height() -> bool:
"""μν μ΄κ³Ό μ¬λΆ νλ¨"""
global CUR_HEIGHT_STEP
v = _read_height_from_file() # current_height.txt μ½κΈ°
if v is None: # νμΌ μ€λ₯/λ²μ μΈ
print("[ACTUATOR] π« λμ΄ νμΌ μ€λ₯. μ΄λ μ°¨λ¨")
return True
CUR_HEIGHT_STEP = v # μ μ μνμ λκΈ°ν
return CUR_HEIGHT_STEP >= HEIGHT_MAX # 28000 step
def exceed_min_height() -> bool:
"""νν μ΄κ³Ό(λ°λ₯) μ¬λΆ νλ¨"""
global CUR_HEIGHT_STEP
v = _read_height_from_file()
if v is None:
print("[ACTUATOR] π« λμ΄ νμΌ μ€λ₯. μ΄λ μ°¨λ¨")
return True
CUR_HEIGHT_STEP = v
return CUR_HEIGHT_STEP <= HEIGHT_MIN # 0 step
# 2οΈβ£ μλ‘ μ΄λ (νμ₯) - λ€μ€ κ²μ¦
def moveUp(steps: int = DEFAULT_STEP):
"""μ‘μΆμμ΄ν° μλ‘ μ΄λ (μΌκ΅΄μ΄ νλ©΄ μλμ μμ λ)"""
global CUR_HEIGHT_STEP
# 첫 λ²μ§Έ κ²μ¦: μ΄λ μμ μ
if exceed_max_height():
print("[ACTUATOR] π« μ΅λ λμ΄ λλ¬, μ΄λ μ€λ¨")
return
GPIO.output(DIR, GPIO.LOW) # μμͺ½ λ°©ν₯
GPIO.output(ENA, GPIO.HIGH) # λͺ¨ν° νμ±ν
print(f"[ACTUATOR] β Move UP {steps} steps (Current: {CUR_HEIGHT_STEP}/{HEIGHT_MAX})")
# λ§€ μ€ν
λ§λ€ νμ€ + μ¬κ²μ¦
for _ in range(steps):
# λ λ²μ§Έ κ²μ¦: κ° μ€ν
λ§λ€
if exceed_max_height():
print("[ACTUATOR] π« μ΅λ λμ΄ λλ¬, μ΄λ μ€λ¨")
break
# νμ€ μ νΈ (μ€ν
λͺ¨ν° ꡬλ)
GPIO.output(PUL, GPIO.HIGH)
sleep(STEP_DELAY) # 0.0004μ΄
GPIO.output(PUL, GPIO.LOW)
sleep(STEP_DELAY)
# μ€μκ° λμ΄ μΆμ λ° μ μ₯
CUR_HEIGHT_STEP += 1
_write_height_to_file(CUR_HEIGHT_STEP)
print(f"[ACTUATOR] β Current step: {CUR_HEIGHT_STEP}/{HEIGHT_MAX}")
GPIO.output(ENA, GPIO.LOW) # λͺ¨ν° λΉνμ±ν
# 3οΈβ£ μλλ‘ μ΄λ (μμΆ) - λ€μ€ κ²μ¦
def moveDown(steps: int = DEFAULT_STEP):
"""μ‘μΆμμ΄ν° μλλ‘ μ΄λ (μΌκ΅΄μ΄ νλ©΄ μμ μμ λ)"""
global CUR_HEIGHT_STEP
# 첫 λ²μ§Έ κ²μ¦: μ΄λ μμ μ
if exceed_min_height():
print("[ACTUATOR] π« μ΅μ λμ΄ λλ¬, μ΄λ μ€λ¨")
return
GPIO.output(DIR, GPIO.HIGH) # μλμͺ½ λ°©ν₯
GPIO.output(ENA, GPIO.HIGH)
print(f"[ACTUATOR] β Move DOWN {steps} steps (Current: {CUR_HEIGHT_STEP}/{HEIGHT_MAX})")
for _ in range(steps):
# λ λ²μ§Έ κ²μ¦: κ° μ€ν
λ§λ€
if exceed_min_height():
print("[ACTUATOR] π« μ΅μ λμ΄ λλ¬, μ΄λ μ€λ¨")
break
GPIO.output(PUL, GPIO.HIGH)
sleep(STEP_DELAY)
GPIO.output(PUL, GPIO.LOW)
sleep(STEP_DELAY)
CUR_HEIGHT_STEP -= 1
CUR_HEIGHT_STEP = max(HEIGHT_MIN, CUR_HEIGHT_STEP)
_write_height_to_file(CUR_HEIGHT_STEP)
print(f"[ACTUATOR] β Current step: {CUR_HEIGHT_STEP}/{HEIGHT_MAX}")
GPIO.output(ENA, GPIO.LOW)
# 4οΈβ£ νλ‘κ·Έλ¨ μ’
λ£ μ μλ μμ 볡κ·
def return_to_start():
"""νμ¬ μμΉλ§νΌ νκ°νμ¬ κΈ°κ³ μμ (0) 볡κ·"""
global CUR_HEIGHT_STEP
if CUR_HEIGHT_STEP <= 0:
print("[ACTUATOR] Already at home position (0 step)")
return
print(f"[ACTUATOR] π Returning to home: {CUR_HEIGHT_STEP} steps down...")
GPIO.output(ENA, GPIO.HIGH)
GPIO.output(DIR, GPIO.HIGH) # νκ° λ°©ν₯
for i in range(CUR_HEIGHT_STEP):
GPIO.output(PUL, GPIO.HIGH)
sleep(STEP_DELAY)
GPIO.output(PUL, GPIO.LOW)
sleep(STEP_DELAY)
_write_height_to_file(CUR_HEIGHT_STEP - i - 1)
if i % 100 == 0 and i > 0:
print(f" β μ§ν: {i}/{CUR_HEIGHT_STEP}")
CUR_HEIGHT_STEP = 0
_write_height_to_file(CUR_HEIGHT_STEP)
GPIO.output(ENA, GPIO.LOW)
print("[ACTUATOR] β
Returned to home position (step=0)")
def on_shutdown():
"""λΉμ μ μ’
λ£ μμλ μμ λ³΅κ· λ³΄μ₯"""
print("[SYSTEM] π» Returning actuator to 0...")
return_to_start()
GPIO.cleanup()
print("[SYSTEM] β
Actuator returned to home position.")
atexit.register(on_shutdown) # μ’
λ£ ν
λ±λ‘- PCA(μ£Όμ±λΆ λΆμ)λ₯Ό ν΅ν΄ μΌκ΅΄μ 3D μ’νκ³λ₯Ό μμ±νκ³ , μμ μμ 벑ν°λ₯Ό μ΅ν©νμ¬ νλ©΄ μ’νλ‘ λ³νν¨μΌλ‘μ¨ κ³ κ° νμ μλ μ νν μμ μΆμ μ μννλ ν΅μ¬ λ‘μ§μ λλ€.
- μ°Έκ³ μ€νμμ€: https://github.com/JEOresearch/EyeTracker
# eye_tracking_worker.py - 3D κΈ°νν κΈ°λ° μμ μΆμ
# 1οΈβ£ PCAλ‘ μΌκ΅΄ 3D μ’νκ³ μμ±
def compute_coordinate_box(face_landmarks, indices, w, h):
# 23κ° μ½ λλλ§ν¬λ‘ μ’νκ³ κ΅¬μ±
points_3d = np.array([
[face_landmarks[i].x * w,
face_landmarks[i].y * h,
face_landmarks[i].z * w]
for i in nose_indices # 23κ° μ
])
# μ£Όμ±λΆ λΆμμΌλ‘ μΌκ΅΄ νμ κ³μ°
center = np.mean(points_3d, axis=0)
cov = np.cov((points_3d - center).T)
eigvals, eigvecs = np.linalg.eigh(cov)
# νμ νλ ¬ μμ± (μΌκ΅΄μ λ°©ν₯)
R_final = Rscipy.from_matrix(eigvecs).as_matrix()
return center, R_final, points_3d
# 2οΈβ£ μμ μμ λ²‘ν° μ΅ν©
def track_gaze():
# μ’μ° λμ μμ λ°©ν₯ κ³μ°
left_gaze_dir = iris_3d_left - sphere_world_l
right_gaze_dir = iris_3d_right - sphere_world_r
# λ λμ νκ· μΌλ‘ μ΅μ’
μμ κ²°μ
combined_direction = (left_gaze_dir + right_gaze_dir) / 2
combined_direction /= np.linalg.norm(combined_direction)
# μμ λ°©ν₯ β νλ©΄ μ’ν λ³ν
screen_x, screen_y = convert_gaze_to_screen_coordinates(
combined_direction,
calibration_offset_yaw, # μ¬μ©μλ³ λ³΄μ κ°
calibration_offset_pitch
)
# 3οΈβ£ μ¬μ©μλ³ μΊλ¦¬λΈλ μ΄μ
def calibrate():
# νμ¬ λ μμΉλ₯Ό κΈ°μ€μ μΌλ‘ μ μ₯
left_sphere_local_offset = R_final.T @ (iris - head_center)
left_sphere_local_offset += base_radius * camera_dir_local
# νλ©΄ μ€μμ λ³΄κ³ μλ€κ³ κ°μ νκ³ μ€νμ
κ³μ°
calibration_offset_yaw = -raw_yaw
calibration_offset_pitch = -raw_pitch- μ¬μ©μ λ°νλ₯Ό λ°μ Gemini LLMμ ν΅ν΄ μλ΅μ μμ±νκ³ , μ£Όλ¬Έ μμ²κ³Ό μΌλ° λνλ₯Ό ꡬλΆνμ¬ μ²λ¦¬νλ λ°±μλ μ±λ΄ μλΉμ€μ ν΅μ¬ λ‘μ§μ λλ€. MSA ꡬ쑰μ λ°λΌ μ£Όλ¬Έ λ°μ μ Order Serviceμ ν΅μ ν©λλ€.
// [ν΅μ¬ λ‘μ§] ChatService.processChat()
// - conversationRepository: μΈμ
λ³ λν κΈ°λ‘ μ‘°ν λ° μ μ₯
// - geminiPromptService: λ©λ΄ μ 보, λν νμ€ν 리 λ±μ μ‘°ν©νμ¬ LLM ν둬ννΈ μμ±
// - geminiClient: Google Gemini API νΈμΆ
// - orderServiceClient: μ£Όλ¬Έ μμ² λ°μ μ μΈλΆ Order Service API νΈμΆ
@Transactional
public ChatResponse processChat(Long storeId, String sessionId, String userMessage, String managedStoreIds, String storeName) {
// 1οΈβ£ λν κΈ°λ‘ μ‘°ν λλ μμ± (μΈμ
κΈ°λ° λν κ΄λ¦¬)
Conversation conversation = conversationRepository.findBySessionId(sessionId)
.orElseGet(() -> new Conversation(sessionId));
// 2οΈβ£ νμ¬ μ¬μ©μ λ©μμ§λ₯Ό λν κΈ°λ‘μ μΆκ°
conversation.addMessage(Message.of("USER", userMessage));
// 3οΈβ£ Geminiμ λ³΄λΌ ν둬ννΈ μμ± (μμ€ν
ν둬ννΈ + λ©λ΄ λ°μ΄ν° + λν νμ€ν 리)
String prompt = geminiPromptService.createPrompt(storeId, conversation, managedStoreIds);
// 4οΈβ£ Gemini API νΈμΆνμ¬ AIμ μλ³Έ μλ΅ λ°κΈ°
GeminiResponse geminiResponse = geminiClient.call(new GeminiRequest(prompt));
String aiRawResponse = geminiResponse.extractText();
// 5οΈβ£ AI μλ΅ λΆμ ν μ΅μ’
λ©μμ§ κ²°μ
String finalAiMessage;
Optional<OrderRequestDto> orderRequestOpt = parseOrderAction(aiRawResponse, storeId, storeName);
if (orderRequestOpt.isPresent()) {
// 5-1. μ£Όλ¬Έ μμ²μΈ κ²½μ°: Order Service νΈμΆ
OrderRequestDto orderRequest = orderRequestOpt.get();
try {
var orderApiResponse = orderServiceClient.placeOrder(orderRequest);
finalAiMessage = "μ£Όλ¬Έμ΄ μλ£λμμ΅λλ€. μ£Όλ¬Έλ²νΈλ " + orderApiResponse.getData().getOrderNumber() + "μ
λλ€.";
} catch (Exception e) {
finalAiMessage = "μ£Όλ¬Έ μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€. λ€μ μλν΄ μ£ΌμΈμ.";
}
} else {
// 5-2. μΌλ° λνμΈ κ²½μ°: Gemini μλ΅ κ·Έλλ‘ μ¬μ©
finalAiMessage = aiRawResponse;
}
// 6οΈβ£ μ΅μ’
AI μλ΅μ λν κΈ°λ‘μ μ μ₯
conversation.addMessage(Message.of("AI", finalAiMessage));
conversationRepository.save(conversation);
// 7οΈβ£ ν΄λΌμ΄μΈνΈμ μ λ¬ν μ΅μ’
μλ΅ μμ±
return new ChatResponse(conversation.getSessionId(), finalAiMessage);
}





