KLingo Project Documentation 1.0.0
Unreal Engine 5.6 C++ Project Documentation
로딩중...
검색중...
일치하는것 없음
ChatInputBox.cpp
이 파일의 문서화 페이지로 가기
1// Copyright (c) 2025 Doppleddiggong. All rights reserved. Unauthorized copying, modification, or distribution of this file, via any medium is strictly prohibited. Proprietary and confidential.
2
3
4#include "ChatInputBox.h"
5
6#include "APlayerActor.h"
7#include "APlayerControl.h"
8#include "ChatWidget.h"
9#include "GameLogging.h"
10#include "MiniOwlBot.h"
11#include "NetworkData.h"
13#include "UPopupManager.h"
14#include "UPopup_DailyStudy.h"
15#include "UGameDataManager.h"
17#include "Components/Button.h"
18#include "Components/MultiLineEditableTextBox.h"
19#include "Framework/Application/SlateApplication.h"
20#include "Onepiece/Onepiece.h"
21
22
23
24
26{
27 Super::NativeOnInitialized();
28
29 Button_Send->OnClicked.AddDynamic(this, &UChatInputBox::HandleSendClicked);
30}
31
32void UChatInputBox::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
33{
34 Super::NativeTick(MyGeometry, InDeltaTime);
35
36 // 현재 포커스 상태 확인
37 bool bIsFocused = MultiLineEditableTextBox_Input && MultiLineEditableTextBox_Input->HasKeyboardFocus();
38
39 // 포커스 상태 변화 감지
40 if (bWasFocused != bIsFocused)
41 {
42 PRINTLOG(TEXT("[ChatInputBox] Focus changed: %d -> %d"), bWasFocused, bIsFocused);
43
44 // 포커스를 잃었을 때만 처리 (화면 클릭 등)
45 if (bWasFocused && !bIsFocused)
46 {
47 PRINTLOG(TEXT("[ChatInputBox] Focus lost - switching to GameOnly mode"));
48
49 // GameOnly 모드로 전환
50 if (APlayerControl* PC = Cast<APlayerControl>(GetOwningPlayer()))
51 {
52 FInputModeGameOnly InputMode;
53 PC->SetInputMode(InputMode);
54 PC->SetShowMouseCursor(false);
55 FSlateApplication::Get().SetAllUserFocusToGameViewport();
56 }
57
58 // ChatWidget에 포커스 해제 알림
60 {
61 PRINTLOG(TEXT("[ChatInputBox] Notifying ChatWidget of focus loss"));
62 OwningChatWidget->OnInputFocusChanged(false);
63 }
64 else
65 {
66 PRINTLOG(TEXT("[ChatInputBox] ERROR: OwningChatWidget is null!"));
67 }
68 }
69 }
70
71 bWasFocused = bIsFocused;
72}
73
74FReply UChatInputBox::NativeOnPreviewKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
75{
76 // Enter 키 감지 & Shift+Enter는 줄바꿈 허용
77 if (InKeyEvent.GetKey() == EKeys::Enter && !InKeyEvent.IsShiftDown())
78 {
80 return FReply::Handled();
81 }
82
83 // ESC 키로 포커스 해제
84 if (InKeyEvent.GetKey() == EKeys::Escape)
85 {
86 SetInputFocus(false);
87 return FReply::Handled();
88 }
89
90 return Super::NativeOnPreviewKeyDown(InGeometry, InKeyEvent);
91}
92
94{
95 // message에 저장 & 텍스트 칸 비우기
96 FText message = MultiLineEditableTextBox_Input->GetText();
97
98 MultiLineEditableTextBox_Input->SetText(FText::GetEmpty());
99
100 return message;
101}
102
104{
105 if ( bFocus )
106 {
107 // 먼저 포커스 설정
108 MultiLineEditableTextBox_Input->SetKeyboardFocus();
109
110 // UI 입력 모드로 전환
111 if (APlayerControl* PC = Cast<APlayerControl>(GetOwningPlayer()))
112 {
113 FInputModeUIOnly InputMode;
114 InputMode.SetWidgetToFocus(MultiLineEditableTextBox_Input->TakeWidget());
115 PC->SetInputMode(InputMode);
116 PC->SetShowMouseCursor(true);
117 }
118
119 // ChatWidget에 포커스 획득 알림
121 {
122 OwningChatWidget->OnInputFocusChanged(true);
123 }
124
125 bWasFocused = true;
126 }
127 else
128 {
129 // GameOnly 모드로 전환 및 포커스 해제
130 if (APlayerControl* PC = Cast<APlayerControl>(GetOwningPlayer()))
131 {
132 FInputModeGameOnly InputMode;
133 PC->SetInputMode(InputMode);
134 PC->SetShowMouseCursor(false);
135
136 // 뷰포트로 포커스 이동 (자동으로 입력창 포커스 해제됨)
137 FSlateApplication::Get().SetAllUserFocusToGameViewport();
138 }
139
140 // ChatWidget에 포커스 해제 알림
142 {
143 OwningChatWidget->OnInputFocusChanged(false);
144 }
145
146 bWasFocused = false;
147 }
148}
149
150bool UChatInputBox::IsAIAsk(const FString& InMessage, FString& OutQuestion) const
151{
152 FString LeftPart, RightPart;
153
154 // 콜론(:)을 기준으로 분리
155 if (InMessage.Split(TEXT(":"), &LeftPart, &RightPart))
156 {
157 // 왼쪽 파트의 공백을 제거하고 "AI"와 일치하는지 확인 (대소문자 무시)
158 if (LeftPart.TrimStartAndEnd().Equals(DefineData::AI, ESearchCase::IgnoreCase))
159 {
160 OutQuestion = RightPart.TrimStart();
161 return !OutQuestion.IsEmpty(); // 내용이 비어있지 않아야 true
162 }
163 }
164 return false;
165}
166
167bool UChatInputBox::IsDailyAsk(const FString& InMessage, FString& OutQuestion) const
168{
169 FString LeftPart, RightPart;
170
171 if (InMessage.Split(TEXT(":"), &LeftPart, &RightPart))
172 {
173 if (LeftPart.TrimStartAndEnd().Equals(DefineData::Daily, ESearchCase::IgnoreCase))
174 {
175 OutQuestion = RightPart.TrimStart();
176 return !OutQuestion.IsEmpty(); // 내용이 비어있지 않아야 true
177 }
178 }
179 return false;
180}
181
183{
184 // 텍스트가 비어있지 않다면 처리
185 FText Message = FlushMessage();
186 if (Message.IsEmpty())
187 {
188 // 빈 메시지면 포커스만 해제
189 SetInputFocus(false);
190 return;
191 }
192
193 auto* PC = Cast<APlayerControl>(GetWorld()->GetFirstPlayerController());
194 if (!PC)
195 return;
196
197 const FString MessageStr = Message.ToString();
198 FString CleanQuestion;
199
200 if (IsAIAsk(MessageStr, CleanQuestion))
201 {
202 // 사용자가 입력한 전체 메시지 표시 (로그성)
203 PC->ServerRPC_SendChat(Message);
204
205 // AI에게 정제된 질문 전송
206 PC->ServerRPC_SendAIQuestion(CleanQuestion);
207
208 PRINTLOG(TEXT("[AI Chat] User question: %s"), *CleanQuestion);
209 }
210 else if (IsDailyAsk(MessageStr, CleanQuestion))
211 {
212 // Daily 단어 생성 요청
213 if (UKLingoNetworkSystem* NetworkSystem = UKLingoNetworkSystem::Get(GetWorld()))
214 {
215 NetworkSystem->RequestDailyQuestion(DefineData::DailySystemPrompt, CleanQuestion,
216 FResponseChatDailysDelegate::CreateUObject(this, &UChatInputBox::OnDailyAnswerReceived));
217
218 PRINTLOG(TEXT("[Daily] Word generation request: %s"), *CleanQuestion);
219 }
220 }
221 else
222 {
223 // 2. 일반 채팅 메시지 전송
224 PC->ServerRPC_SendChat(Message);
225 }
226
227 // 메시지 전송 후 포커스 해제 및 게임 모드로 복원
228 SetInputFocus(false);
229
230}
231
236
237TArray<FWordData> UChatInputBox::GetRandomKoreanWords(int32 Count)
238{
239 TArray<FWordData> RandomWordDataArray;
240
241 UGameDataManager* DataManager = UGameDataManager::Get(GetWorld());
242 if (!DataManager)
243 {
244 PRINTLOG(TEXT("[Daily] Error: GameDataManager not found"));
245 return RandomWordDataArray;
246 }
247
248 // ReadData에서 모든 키 가져오기
249 TArray<int32> AllKeys = DataManager->GetAllReadDataKeys();
250
251 if (AllKeys.Num() == 0)
252 {
253 PRINTLOG(TEXT("[Daily] Error: No ReadData available"));
254 return RandomWordDataArray;
255 }
256
257 // 요청한 개수만큼 랜덤 단어 선택
258 int32 WordsToGenerate = FMath::Min(Count, AllKeys.Num());
259
260 for (int32 i = 0; i < WordsToGenerate; ++i)
261 {
262 // 랜덤 인덱스 선택
263 int32 RandomIndex = FMath::RandRange(0, AllKeys.Num() - 1);
264 int32 RandomKey = AllKeys[RandomIndex];
265 AllKeys.RemoveAt(RandomIndex); // 중복 방지
266
267 // 데이터 로드
268 FReadData ReadData;
269 if (DataManager->GetReadData(RandomKey, ReadData))
270 {
271 // FWordData 생성 (ReadData에는 Pronunciation이 없으므로 Eng로 임시 설정)
272 FWordData WordData;
273 WordData.Kor = ReadData.Word;
274 WordData.Eng = ReadData.Eng;
275 WordData.Pronunciation = ReadData.Eng; // TODO: 나중에 실제 발음 데이터로 교체
276
277 RandomWordDataArray.Add(WordData);
278 }
279 }
280
281 PRINTLOG(TEXT("[Daily] Generated %d random word data from ReadData"), RandomWordDataArray.Num());
282 return RandomWordDataArray;
283}
284
285void UChatInputBox::OnDailyAnswerReceived(FResponseChatDailys& ResponseData, bool bWasSuccessful)
286{
287 const int32 MIN_REQUIRED_WORDS = 3;
288
289 if (!bWasSuccessful)
290 {
291 // 네트워크 실패 시 랜덤 단어 데이터로 대체
292 TArray<FWordData> FallbackWordData = GetRandomKoreanWords(MIN_REQUIRED_WORDS);
293
294 if (FallbackWordData.Num() > 0)
295 {
296 if (UPopup_DailyStudy* DailyStudyPopup = UPopupManager::Get(GetWorld())->ShowPopupAs<UPopup_DailyStudy>(EPopupType::DailyStudy))
297 {
298 DailyStudyPopup->InitPopup(FallbackWordData);
299 }
300 }
301
302 return;
303 }
304
305 // AI 응답을 | 구분자로 파싱 (형식: "개|DOG|Gae|고양이|CAT|Go-yang-i")
306 TArray<FString> RawTokens;
307 ResponseData.answer.ParseIntoArray(RawTokens, TEXT("|"), true);
308
309 // 3개씩 묶어서 FWordData로 변환 (Kor|Eng|Pronunciation)
310 TArray<FWordData> ValidWordDataArray;
311 for (int32 i = 0; i + 2 < RawTokens.Num(); i += 3)
312 {
313 FString Kor = RawTokens[i].TrimStartAndEnd();
314 FString Eng = RawTokens[i + 1].TrimStartAndEnd();
315 FString Pronunciation = RawTokens[i + 2].TrimStartAndEnd();
316
317 // 빈 문자열 체크
318 if (Kor.IsEmpty() || Eng.IsEmpty() || Pronunciation.IsEmpty())
319 {
320 PRINTLOG(TEXT("[Daily] Skipped: Empty field (Kor: '%s', Eng: '%s', Phon: '%s')"), *Kor, *Eng, *Pronunciation);
321 continue;
322 }
323
324 // 한국어 검증 (Kor 필드만)
326 {
327 PRINTLOG(TEXT("[Daily] Skipped: Invalid Korean word '%s'"), *Kor);
328 continue;
329 }
330
331 // FWordData 생성
332 FWordData WordData;
333 WordData.Kor = Kor;
334 WordData.Eng = Eng;
335 WordData.Pronunciation = Pronunciation;
336
337 ValidWordDataArray.Add(WordData);
338 PRINTLOG(TEXT("[Daily] Parsed Word: Kor='%s', Eng='%s', Phon='%s'"), *Kor, *Eng, *Pronunciation);
339 }
340
341 // 유효한 단어가 부족하면 GameDataManager에서 보충
342 if (ValidWordDataArray.Num() < MIN_REQUIRED_WORDS)
343 {
344 int32 WordsNeeded = MIN_REQUIRED_WORDS - ValidWordDataArray.Num();
345
346 TArray<FWordData> AdditionalWordData = GetRandomKoreanWords(WordsNeeded);
347 ValidWordDataArray.Append(AdditionalWordData);
348
349 PRINTLOG(TEXT("[Daily] Added %d fallback words from ReadData"), AdditionalWordData.Num());
350 }
351
352 // 최종 검증 및 팝업 표시
353 if (ValidWordDataArray.Num() > 0)
354 {
355 if (UPopup_DailyStudy* DailyStudyPopup = UPopupManager::Get(GetWorld())->ShowPopupAs<UPopup_DailyStudy>(EPopupType::DailyStudy))
356 {
357 DailyStudyPopup->InitPopup(ValidWordDataArray);
358 PRINTLOG(TEXT("[Daily] Initialized DailyStudy popup with %d words"), ValidWordDataArray.Num());
359 }
360 }
361}
Declares the player-controlled character actor.
APlayerControl 선언에 대한 Doxygen 주석을 제공합니다.
YiSan 전반에서 사용하는 공용 인터페이스를 선언합니다.
#define PRINTLOG(fmt,...)
Definition GameLogging.h:30
네트워크 요청과 응답에 사용되는 구조체 및 설정을 정의합니다.
UCommonFunctionLibrary 클래스를 선언합니다.
UGameDataManager 클래스를 선언합니다.
KLingo API 요청을 담당하는 서브시스템을 선언합니다.
void OnDailyAnswerReceived(FResponseChatDailys &ResponseData, bool bWasSuccessful)
virtual void NativeOnInitialized() override
void SetInputFocus(bool bFocus)
FText FlushMessage()
bool HasKeyboardFocus()
TObjectPtr< class UMultiLineEditableTextBox > MultiLineEditableTextBox_Input
bool IsAIAsk(const FString &InMessage, FString &OutQuestion) const
void HandleSendClicked()
virtual FReply NativeOnPreviewKeyDown(const FGeometry &InGeometry, const FKeyEvent &InKeyEvent) override
TArray< struct FWordData > GetRandomKoreanWords(int32 Count)
GameDataManager에서 랜덤 한국어 단어 데이터 가져오기 (FWordData 형태)
virtual void NativeTick(const FGeometry &MyGeometry, float InDeltaTime) override
bool IsDailyAsk(const FString &InMessage, FString &OutQuestion) const
TObjectPtr< class UButton > Button_Send
TObjectPtr< class UChatWidget > OwningChatWidget
static bool IsValidKoreanWord(const FString &Word)
한국어 단어 검증 (한글만 포함되어 있는지 확인)
데이터 테이블(.csv)에서 게임 데이터를 로드하고 캐시하여 런타임에 빠르게 접근할 수 있도록 제공하는 데이터 관리 서브시스템입니다.
bool GetReadData(int32 Index, FReadData &Out) const
TArray< int32 > GetAllReadDataKeys() const
KLingo 서버와의 HTTP 요청을 중재하는 게임 인스턴스 서브시스템입니다.
Daily Study 팝업
static const FString Daily
Definition Onepiece.h:61
static const FString DailySystemPrompt
Definition Onepiece.h:70
static const FString AI
Definition Onepiece.h:59
읽기 학습 데이터를 정의하는 구조체
Definition FReadData.h:19
FString Eng
학습 단어
Definition FReadData.h:39
FString Word
학습 단어
Definition FReadData.h:32
Chat Dailys 응답 구조체입니다.
단어 데이터 구조체입니다.
FString Kor
FString Pronunciation
FString Eng