-
Notifications
You must be signed in to change notification settings - Fork 0
Feature: 캐릭터 모션 기능 개발 #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough이 PR은 인증·토큰 서브시스템(Guest/OAuth2/토큰 저장·갱신)과 PKCE·플랫폼별 OAuth2 콜백 핸들러를 추가하고, HttpApiClient에 per-request 인증 플래그를 도입하며 WebSocket을 세션→토큰 기반으로 전환하고 WebGL에서 NativeWebSocket을 통합한다. 또한 Live2D 기반 캐릭터 액션 리팩터, 여러 유틸/DTO/예제 스크립트와 에디터/메타/패키지 변경을 포함합니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant UI as UI/예제
participant AuthM as AuthManager
participant Prov as ServerOAuth2Provider
participant CB as CallbackHandler
participant HTTP as HttpApiClient
participant Token as TokenManager
UI->>AuthM: LoginWithOAuth2Async()
AuthM->>Prov: LoginWithServerOAuth2Async()
Prov->>Prov: GeneratePKCE
Prov->>HTTP: StartServerOAuth2Async(pkce)
HTTP-->>Prov: AuthUrl
Prov->>CB: InitializeAsync(expectedState, timeout)
Prov->>User: 브라우저 열기(AuthUrl)
CB-->>Prov: WaitForCallbackAsync() -> callbackUrl
Prov->>Prov: HandleOAuth2Callback(callbackUrl)
Prov->>HTTP: RequestTokenAsync(state)
HTTP-->>Prov: TokenSet
Prov-->>AuthM: TokenSet
AuthM->>Token: SaveTokens(TokenSet)
Token-->>AuthM: OnTokensUpdated
AuthM-->>UI: OnLoginSuccess
note right of Token: 이후 TokenRefreshService가 만료 시 갱신 시도
sequenceDiagram
autonumber
participant WS as WebSocketManager
participant Token as TokenManager
participant Refresh as TokenRefreshService
participant Factory as WebSocketFactory
participant Sock as WebSocket
participant Server as 서버
WS->>Token: HasValidTokens?
alt 토큰 없음/만료
WS->>Refresh: EnsureValidTokenAsync()
Refresh-->>WS: (성공/실패)
opt 실패
WS-->>WS: 연결 중단/종료
end
end
WS->>Factory: CreateWebSocket()
Factory-->>WS: Sock
WS->>Sock: ConnectAsync(url?access_token=...)
Sock-->>WS: OnOpen
Server-->>Sock: Message
Sock-->>WS: OnMessageReceived
WS-->>App: OnMessageReceived(event)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 54
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
Assets/Core/Audio/AudioRecorder.cs (1)
159-183
: OnRecordingStopped 이벤트가 중복(최대 2~3회) 발행됩니다성공 경로에서 한 번,
catch
이후 분기에서 한 번, 그리고finally
에서 다시 한 번 호출될 수 있습니다. 이벤트는 호출당 정확히 1회만 발행되도록 보장해야 합니다.if (_recordingClip != null) { AudioClip? processedClip = ProcessRecordingClip(actualRecordingDuration); if (processedClip != null) { Debug.Log($"[AudioRecorder] 음성 녹음 완료됨 ({actualRecordingDuration:F1}초, {processedClip.samples} 샘플)"); - OnRecordingCompleted?.Invoke(processedClip); - OnRecordingStopped?.Invoke(); + OnRecordingCompleted?.Invoke(processedClip); return processedClip; } } - OnRecordingStopped?.Invoke(); return null; } catch (Exception ex) { Debug.LogError($"[AudioRecorder] 녹음 중지 실패: {ex.Message}"); OnError?.Invoke($"녹음 중지 실패: {ex.Message}"); _isRecording = false; return null; } finally { - // 중복 호출 방지를 위해 성공 분기에서 이미 호출했다면 옵저버 측에서 idempotent 처리 가정 - OnRecordingStopped?.Invoke(); + // 성공/실패 불문하고 한 번만 Stopped 이벤트를 발생 + OnRecordingStopped?.Invoke(); }Assets/Infrastructure/Network/Configs/NetworkConfig.cs (5)
63-76
: 런타임 가드가 호출되지 않아 모바일 빌드에서 환경 강제(Production)가 적용되지 않습니다ApplyRuntimeGuard가 정의만 되어 있고 사용되지 않습니다. 에디터 외 모바일 런타임에서 개발 서버로 붙는 리스크가 큽니다. Instance 초기화 시 호출하도록 연결이 필요합니다.
다음과 같이 Instance getter에 가드를 적용하세요.
if (_instance == null) { _instance = Resources.Load<NetworkConfig>("NetworkConfig"); if (_instance == null) { Debug.LogError("NetworkConfig를 찾을 수 없습니다. Resources 폴더에 NetworkConfig.asset 파일을 생성하세요."); _instance = CreateDefaultInstance(); } + // 런타임 환경 가드 적용 + ApplyRuntimeGuard(_instance); }
125-139
: WebSocket도 wss 미사용WebSocket 주소도 ws://로 고정되어 있습니다. 프로덕션에서는 wss로 전환해야 합니다.
- return $"ws://{server}"; + var scheme = CurrentEnvironment == EnvironmentType.Production ? "wss" : "ws"; + return $"{scheme}://{server}";동일 보정이 GetWebSocketServerAddressFor에도 필요합니다.
166-176
: 쿼리 파라미터 인코딩 누락sessionId를 그대로 URL에 붙이면 특수문자(.) 등으로 인해 연결 실패 가능성이 있습니다. URL 인코딩 하세요.
- return $"{baseWsUrl}?sessionId={sessionId}"; + return $"{baseWsUrl}?sessionId={Uri.EscapeDataString(sessionId ?? string.Empty)}";
116-123
: Production 환경에서 HTTP/WS 하드코딩 제거 및 TLS 분기 적용
- Assets/Infrastructure/Network/Configs/NetworkConfig.cs (121, 137, 151):
http://
→ 환경에 따라https://
사용ws://
→ 환경에 따라wss://
사용- 예시
- return $"http://{server}"; + var scheme = CurrentEnvironment == EnvironmentType.Production ? "https" : "http"; + return $"{scheme}://{server}"; - return $"ws://{server}"; + var wsScheme = CurrentEnvironment == EnvironmentType.Production ? "wss" : "ws"; + return $"{wsScheme}://{server}";
206-216
: API 버전 프리픽스 처리 일관성 확보
GetFullApiUrl
및 헬퍼(GetUserApiUrl
등)가 버전 프리픽스를 제거했으나, 실제 서비스 코드에서는 여전히"/api/v1/…"
를 직접 사용 중입니다. 버전 누락 시 404 오류 발생 위험이 있습니다.- 아래 중 하나로 통일 적용하세요:
- 헬퍼 내부에
"/api/{ApiVersion}/"
포함하도록 수정(또는GetVersionedApiUrl
신규 도입)- 서비스 코드의 직접 경로를 헬퍼 호출로 전환
- README 예제도 함께 갱신 필요
Assets/Core/DebugConsole/GameDebugConsoleManager.cs (1)
333-340
: 런타임에서 DestroyImmediate 사용 금지DestroyImmediate는 에디터 전용에 가깝고 런타임에서 프레임 안전성을 해칩니다. Destroy로 교체하세요.
- DestroyImmediate(obj); + Destroy(obj);Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs (1)
32-66
: ConnectAsync에서 CancellationToken을 사용하지 않아 취소가 동작하지 않습니다_cancellationTokenSource를 보유하지만 Connect()에 연결하지 않아 OperationCanceledException catch는 의미가 없습니다.
다음처럼 외부 토큰과 내부 토큰을 연결해 취소를 전달하세요.
- await _webSocket.Connect(); + var linked = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, _cancellationTokenSource.Token).Token; + // Task → UniTask 변환 후 외부 취소 연결 + await _webSocket.Connect().AsUniTask().AttachExternalCancellation(linked);
♻️ Duplicate comments (2)
Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs.meta (1)
1-2
: 스크립트 .meta 포맷 최소화 — 팀/CI 간 일관성 위해 에디터 재생성 추천Unity가 자동으로 추가 키를 채우는 과정에서 불필요한 변경이 생길 수 있습니다. 에디터에서 Reimport 후 .meta 변경분이 생기면 커밋해 주세요. 동일 점검 스크립트는 상단 코멘트 참조.
Assets/Infrastructure/Network/DTOs/Auth/GuestLoginResponse.cs.meta (1)
1-2
: 스크립트 .meta 최소 키만 포함 — Reimport로 안정화 권장MonoImporter 블록 존재 여부 점검 및 필요 시 .meta 재생성/커밋을 권장합니다. 상단 제공 스크립트로 일괄 확인 가능.
🧹 Nitpick comments (118)
Assets/Infrastructure/Auth/OAuth2/Config.meta (1)
1-8
: OAuth2 설정 폴더 내 비밀정보 커밋 금지이 폴더는 설정 용도이므로 client_secret 등 민감값을 절대 자산(.asset)으로 커밋하지 마세요. 퍼블릭 클라이언트(게임 클라이언트)는 secret을 보관하면 안 됩니다. 환경별 값은:
- 런타임 환경변수/원격 설정(예: Remote Config)에서 주입
- 필요 시 예시 템플릿(README/Example.asset)만 버전관리, 실제 값은 .gitignore
원하시면 README 템플릿/스크립트 작성 도와드릴게요.
Assets/Settings/Build Profiles/New Windows Profile.asset.meta (1)
1-8
: 자잘한 네이밍 제안: 'New' 접두어 제거프로파일이 확정된 상태라면 파일명을 'Windows Profile' 등으로 고정해 이력 잡음과 병합 충돌을 줄이는 것을 권장합니다.
Assets/Infrastructure/Auth/OAuth2/Utils.meta (1)
7-8
: AssetBundle 이름/변형 공란 확인의도적으로 번들 대상이 아니라면 그대로 OK입니다. 만약 폴더 단위 번들링 계획이 있다면, 폴더 전체가 하나의 번들로 묶일 수 있으니 주의가 필요합니다.
webgl_sharing (2)
1-1
: 파일 끝 개행(Newline) 추가 권장POSIX/일반 도구 호환성을 위해 EOF 개행을 추가하세요.
적용 diff:
-66c249dd-2064-4e8c-b905-8b87e0a0d086 +66c249dd-2064-4e8c-b905-8b87e0a0d086 +에디터 설정(예: .editorconfig)으로 일괄 강제:
[*] insert_final_newline = true
1-1
: 이 UUID 파일의 용도 문서화해당 디렉터리에 README를 두어 생성/회전 정책과 소비 주체(런타임/에디터/빌드 스텝)를 명시해 주세요. 향후 유지보수, 충돌 해결에 도움이 됩니다.
예시(README.md):
# webgl_sharing - 파일: 66c249dd-2064-4e8c-b905-8b87e0a0d086 (단일 UUID 텍스트) - 사용처: <어디에서 읽는지(스크립트/플러그인/서버 동기화 등)> - 수명주기: <고정/환경별/빌드마다 재생성> - 보안: <비밀 아님/비밀 아님이지만 유추 위험 없음 등>
Assets/Core/Audio/AudioRecorder.cs (3)
2-2
: 파일 레벨 가드와 내부 UNITY_WEBGL 분기가 중복되어 사실상 도달 불가 코드가 존재합니다현재
#if !UNITY_WEBGL || UNITY_EDITOR
로 파일 전체가 WebGL 플레이어 빌드에서 제외되므로, 내부의#if UNITY_WEBGL && !UNITY_EDITOR
분기들은 컴파일/실행될 기회가 없습니다. 유지보수 단순화를 위해 아래 둘 중 하나로 정리하는 것을 권장합니다.
- 옵션 A: 파일 레벨 가드를 제거하고, 메서드/프로퍼티에 WebGL 분기(경고/No-Op)를 유지
- 옵션 B(간단): 파일 레벨 가드를 유지하고, 내부의 WebGL 분기 코드를 제거
40-45
: IsRecordingAvailable의 불필요한 플랫폼 분기 제거 제안파일 레벨 가드로 WebGL 플레이어에선 이 타입이 없어지므로, 이 프로퍼티의 UNITY_WEBGL 분기는 절대 사용되지 않습니다. 단순화해 주세요.
- public bool IsRecordingAvailable => -#if UNITY_WEBGL && !UNITY_EDITOR - false; -#else - Microphone.devices.Length > 0; -#endif + // WebGL 플레이어에선 파일 레벨 가드로 타입이 제외됨 + public bool IsRecordingAvailable => Microphone.devices.Length > 0;
107-115
: StartRecording 내부 WebGL 분기 도달 불가 → 정리 권장파일 레벨 가드 때문에 아래 UNITY_WEBGL 분기는 컴파일되지 않습니다. 불필요한 분기를 제거하여 가독성과 유지보수성을 개선하세요.
-#if UNITY_WEBGL && !UNITY_EDITOR - Debug.LogWarning("[AudioRecorder] WebGL에서는 마이크로폰이 지원되지 않습니다."); - OnError?.Invoke("WebGL에서는 음성 녹음이 지원되지 않습니다."); - _isRecording = false; - return false; -#else _recordingClip = Microphone.Start(_currentDevice ?? string.Empty, false, _maxRecordingLength, _sampleRate); -#endifAssets/Domain/Character/Script/Component/ResolutionManager.cs (4)
34-37
: 런타임 해상도 변경 폴링 비활성화 — 필요 시 토글 가능하도록 유지 제안Update에서 해상도 변경 감지가 완전히 꺼져 있습니다. 상위 오케스트레이터가 이벤트로 호출해 준다면 OK지만, 필요 시 인스펙터 토글로 폴링을 켜둘 수 있게 하면 운영이 유연해집니다.
@@ void Update() - // if (applyOnResolutionChange && HasResolutionChanged()) - // { - // ApplyScaleToAllModels(); - // } + if (pollResolutionChange && applyOnResolutionChange && HasResolutionChanged()) + { + ApplyScaleToAllModels(); + }필드 추가:
[SerializeField] private bool pollResolutionChange = false;
95-101
: Initialize 미호출 시 안전장치 추가: scaleConfig 지연 로드외부에서 Initialize 없이 곧바로 ApplyScaleToAllModels를 호출할 경우, 현재는 경고만 내고 리턴합니다. 서비스 안정성을 위해 이 지점에서 지연 로드/초기화를 수행하도록 개선을 권장합니다.
- if (scaleConfig == null) - { - Debug.LogWarning("[ResolutionManager] 스케일 설정이 없습니다."); - return; - } + if (scaleConfig == null) + { + scaleConfig = Resources.Load<ResolutionModelScaleConfig>("ResolutionModelScaleConfig"); + if (scaleConfig == null) + { + Debug.LogWarning("[ResolutionManager] 스케일 설정(ResolutionModelScaleConfig)을 찾을 수 없습니다."); + return; + } + }
112-121
: 적용 중 컬렉션 변경 안전성 확보스케일 적용 도중 Register/Unregister가 개입될 경우(같은 프레임 내), foreach가 예외를 유발할 수 있습니다. 스냅샷을 대상으로 순회하도록 변경을 권장합니다.
- foreach (var scaler in modelScalers) + foreach (var scaler in modelScalers.ToArray()) { if (scaler != null) { // 스케일러에 미리 계산된 스케일을 전달 scaler.ApplyScaleWithPreCalculatedScale(scale); } }
17-18
: 사용되지 않는 토글 혼동 최소화applyOnResolutionChange는 현재 Update 경로가 주석 처리되어 실사용되지 않습니다. 툴팁으로 현재 동작을 명시하거나, 상단에 주석으로 “수동 오케스트레이션 전환”을 설명해 두면 혼선을 줄일 수 있습니다.
Assets/Domain/Character/Script/Component/CameraResolutionScaler.cs (6)
29-29
: Start에서 Initialize 비활성화 — applyOnStart가 무력화됨Start가 Initialize를 호출하지 않으므로, applyOnStart 값은 더 이상 의미가 없습니다. 의도라면 외부 오케스트레이터가 Initialize를 호출하도록 확실히 보장해야 합니다. 선택적 자동 초기화 토글을 제안합니다.
@@ void Start() - // Initialize(); + if (autoInitialize) + { + Initialize(); + }필드 추가:
[SerializeField] private bool autoInitialize = false;
34-37
: 해상도 변경 폴링 제어 토글 추가 제안운영 중 해상도/윈도우 크기/회전 변경 대응이 필요할 수 있습니다. 인스펙터에서 손쉽게 켜고 끌 수 있도록 토글을 권장합니다.
@@ void Update() - // if (applyOnResolutionChange && HasResolutionChanged()) - // { - // ApplyCameraScale(); - // } + if (pollResolutionChange && applyOnResolutionChange && HasResolutionChanged()) + { + ApplyCameraScale(); + }필드 추가:
[SerializeField] private bool pollResolutionChange = false;
84-88
: 직교 카메라 전제 조건 검증Orthographic Size를 조정하므로, 카메라가 직교 모드가 아닐 경우 경고를 남기거나 조정 자체를 생략하는 것이 안전합니다.
@@ public void ApplyCameraScale() - if (targetCamera == null) + if (targetCamera == null) { Debug.LogWarning("[CameraResolutionScaler] 카메라가 없습니다."); return; } + if (!targetCamera.orthographic) + { + Debug.LogWarning("[CameraResolutionScaler] 대상 카메라가 Orthographic 모드가 아닙니다. 스케일 적용을 건너뜁니다."); + return; + }
93-99
: 스케일 기준값으로 baseOrthographicSize 대신 원본값 사용 권장디자인 타임에서 설정된 카메라 크기를 보존하려면 originalOrthographicSize를 기준으로 스케일을 적용하는 편이 일관됩니다. 현재는 직렬화된 baseOrthographicSize(기본 5f)에 고정되어 있어 실제 씬 설정과 어긋날 수 있습니다.
- targetCamera.orthographicSize = baseOrthographicSize * scale; + targetCamera.orthographicSize = originalOrthographicSize * scale;
130-134
: 현재 카메라 스케일 계산 기준 정합성위 변경과 함께 GetCurrentCameraScale도 원본값 대비 비율을 반환하도록 맞추는 것이 좋습니다.
- if (targetCamera == null) return 1f; - return targetCamera.orthographicSize / baseOrthographicSize; + if (targetCamera == null) return 1f; + return originalOrthographicSize == 0f ? 1f : targetCamera.orthographicSize / originalOrthographicSize;
181-186
: 미사용 변수 정리 또는 활용baseAspect를 계산하지만 사용하지 않습니다. 정리하거나, 세로/가로 판단을 기준 해상도 대비로 바꾸려면 baseAspect를 활용하세요.
예) 기준 해상도 대비로 판단:
- if (currentAspect < 1.0f) // 세로 모드 + if (currentAspect < baseAspect) // 기준 대비 세로형Assets/Infrastructure/Auth/OAuth2/ServerOAuth2Provider.cs.meta (1)
1-2
: 메타 등록 이상 없음 (EOF 개행 니트픽)구성은 정상입니다. 파일 끝 개행이 없다면 스타일 체크에서 걸릴 수 있으니 개행 추가를 권장합니다.
Assets/Infrastructure/Auth/OAuth2/Handlers/WebGLCallbackHandler.cs.meta (1)
1-2
: 메타 정상 + WebGL 전용 핸들러 확인 (EOF 개행 니트픽)설정은 문제 없습니다. 파일 끝 개행이 없다면 팀 코드스타일에 맞춰 추가를 권장합니다.
Assets/Infrastructure/Auth/OAuth2/Handlers/MobileCallbackHandler.cs.meta (1)
1-2
: .meta 파일 무결성 확인 및 추가 권장 사항
- Assets/Infrastructure/Auth 이하 모든
.cs.meta
에 대응하는.cs
파일이 존재하며, 전체 Assets 내 GUID 중복도 없습니다.- 모든
.cs.meta
에서 MonoImporter 블록이 자동 생성되지 않아 의도치 않은 변경 노이즈 우려가 없습니다.- VCS diff 노이즈 방지를 위해
MobileCallbackHandler.cs.meta
등.meta
파일 마지막에 개행이 없는 경우 개행을 추가하세요.- 팀차원에서 동일한 Unity 버전과 Asset Serialization(Force Text) 설정을 고정해 메타 변경 노이즈를 최소화하십시오.
Assets/Infrastructure/Auth/OAuth2/Handlers/OAuth2CallbackHandlerFactory.cs.meta (1)
1-2
: 동일: .cs 페어/중복 GUID/직렬화 정책 확인
- OAuth2CallbackHandlerFactory.cs 실제 존재 여부 확인.
- GUID 전역 중복 없음 확인(상단 스크립트 재사용).
- 팀 공통 Unity 버전과 Force Text 직렬화 고정 권장. EOF 개행 정리 권장.
Assets/Infrastructure/Auth/OAuth2/Handlers/IOAuth2CallbackHandler.cs.meta (1)
1-2
: Unity 버전 간 메타 보완 가능성 안내이 파일도 MonoImporter 블록이 없는 최소 형태입니다. 다른 환경에서 열릴 때 자동 보완으로 diff가 발생할 수 있으니, 팀 표준 Unity 버전 고정 및 메타 재직렬화(Assets/Reimport All) 시점 합의 권장.
Assets/Infrastructure/Auth/TokenRefreshService.cs.meta (1)
1-2
: 런타임 의존 스크립트와의 페어 확인TokenRefreshService.cs가 실제 커밋되어 있고 네임스페이스/경로가 일치하는지, 그리고 리네임/이동 시 GUID 유지 정책을 따르는지 확인 부탁드립니다. 상단 GUID/페어 점검 스크립트로 함께 검증 가능합니다.
Assets/Infrastructure/Auth/Utils/DeviceIdProvider.cs.meta (1)
1-2
: 디바이스ID 민감도 관점에서의 운영 팁해당 .cs가 플랫폼별 전처리기 분기나 네이티브 플러그인 의존이 있다면, 에디터/각 플랫폼 빌드에서 스크립트 포함/제외 설정이 일관적인지 확인해 주세요. 메타 자체는 정상입니다.
Assets/Infrastructure/Auth/OAuth2/Utils/PKCEGenerator.cs.meta (1)
1-2
: Assets/Infrastructure/Auth/OAuth2/Utils/PKCEGenerator.cs.meta 파일 끝에 개행(빈 줄)을 추가해주세요.Assets/Infrastructure/Auth/Examples/AuthManagerExample.cs.meta (2)
1-2
: .cs.meta에 MonoImporter 섹션 누락 가능성
Unity 재임포트로 메타를 정규화했는지 확인해주세요. 누락 시 향후 자동 갱신되며 팀 간 diff가 잦아질 수 있습니다. 위 MonoImporter 점검 스크립트로 확인 가능.
1-2
: EOF 개행 권장
끝 개행이 없다면 추가를 권장합니다. 위 EOF 점검 스크립트 참고.Assets/Infrastructure/Auth/AuthManager.cs.meta (1)
1-2
: .cs.meta 정규화 확인 필요
MonoImporter 섹션 유무를 확인하고, 필요 시 Unity에서 재임포트하여 메타를 안정화하세요. 위 스크립트로 일괄 점검 가능합니다.Assets/Infrastructure/Auth/JwtTokenParser.cs.meta (1)
1-2
: Unity .meta 추가사항 확인 — GUID 안정성만 점검하세요신규 C# 스크립트 메타가 정상입니다. 아래만 확인하면 됩니다:
- 해당 .cs 파일이 동일 경로에 있고 GUID 충돌이 없는지.
- 리포지토리 정책에 따라 메타 파일 말미 개행이 필요한지(현재 없음). 필요 시 개행 추가 권장.
Assets/Infrastructure/Auth/Models/RefreshToken.cs.meta (1)
1-2
: 모델 스크립트 메타 정상 — 사소한 스타일 니트GUID 문제 없어 보입니다. 팀 컨벤션이 “모든 텍스트 파일 끝 개행”이라면 개행 추가만 해 주세요.
Assets/Core/Attribute/ReadOnlyAttribute.cs (1)
3-3
: Attribute 사용 범위 한정 및 네임스페이스/봉인 권장필드 전용으로 한정하고 네임스페이스/봉인을 적용하면 오용을 줄이고 API 표면을 정돈할 수 있습니다. Drawer 쪽에 해당 네임스페이스 using 추가 여부는 함께 확인 부탁드립니다.
권장 변경안:
-using UnityEngine; - -public class ReadOnlyAttribute : PropertyAttribute { } +namespace ProjectVG.Core.Attributes +{ + using UnityEngine; + + [System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public sealed class ReadOnlyAttribute : PropertyAttribute { } +}Drawer(Assets/Core/Attribute/ReadOnlyDrawer.cs)가
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
를 사용한다면, 다음 중 하나가 필요합니다:
using ProjectVG.Core.Attributes;
추가, 또는typeof(ProjectVG.Core.Attributes.ReadOnlyAttribute)
로 전체 한정 이름 사용.Assets/Infrastructure/Auth/Examples.meta (1)
1-8
: Examples 폴더의 빌드 포함 여부 관리 제안샘플 코드는 플레이어 빌드 제외가 일반적입니다. Editor 전용 asmdef(예: ProjectVG.Auth.Examples)로 분리하거나 Editor 하위 폴더로 이동해 빌드 체인에서 배제하는 구성을 권장합니다.
Assets/Infrastructure/Auth/OAuth2.meta (1)
1-8
: GUID 중복 점검 스크립트 개선
fd와 xargs 사용 시 파일 경로에 공백·특수문자가 포함되면 경로가 잘못 분리되어 오류가 발생하므로, null 구분자 옵션(-0)을 추가해 안전하게 처리하도록 스크립트를 수정하세요.#!/usr/bin/env bash set -euo pipefail # .meta 파일 목록을 null 구분자로 수집 mapfile -d '' files < <(fd -HI -t f -e meta -0) # GUID 추출 declare -a guids for f in "${files[@]}"; do if [[ $(grep -E '^guid:' "$f") =~ ^guid:\ ([a-f0-9]{32})$ ]]; then guids+=("${BASH_REMATCH[1]}") fi done # 중복 GUID 확인 duplicates=$(printf '%s\n' "${guids[@]}" | sort | uniq -d) if [[ -z "$duplicates" ]]; then echo "No duplicate GUIDs." exit 0 fi echo "Duplicate GUIDs found:" for g in $duplicates; do printf 'GUID: %s\n' "$g" for f in "${files[@]}"; do grep -q "guid: $g" "$f" && printf ' %s\n' "$f" done done exit 1.gitignore (2)
133-133
: 실제 ‘WebGL Builds’ 디렉터리가 존재하지 않아 .gitignore의/WebGL Builds
항목을 제거해 주세요.
56-64
:*claude*
패턴 제거
세부 패턴이 이미 충분하므로 과도한 catch-all인*claude*
만 제거해주세요.# Claude AI related files CLAUDE.md claude.md *CLAUDE.md *claude.md .claude/ claude-* -*claude*
Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs (1)
28-30
: 간단 XML 주석 추가 제안사용 의도가 자주 혼동되는 API라 요약 주석을 권장합니다.
- // 메시지 큐 처리 (NativeWebSocket 패키지용) - void DispatchMessageQueue(); + /// <summary> + /// NativeWebSocket(WebGL)에서 수신 이벤트를 메인 스레드로 펌프하기 위해 매 프레임 호출합니다. + /// WebGL 이외 플랫폼에서는 일반적으로 필요 없습니다. + /// </summary> + void DispatchMessageQueue();Assets/Infrastructure/Network/Configs/NetworkConfig.cs (1)
203-205
: ToLower 문화권 종속성메시지 타입 비교 시 ToLower() 대신 ToLowerInvariant() 또는 OrdinalIgnoreCase 비교를 권장합니다.
- public static bool IsJsonMessageType => Instance.wsMessageType?.ToLower() == "json"; - public static bool IsBinaryMessageType => Instance.wsMessageType?.ToLower() == "binary"; + public static bool IsJsonMessageType => + string.Equals(Instance.wsMessageType, "json", StringComparison.OrdinalIgnoreCase); + public static bool IsBinaryMessageType => + string.Equals(Instance.wsMessageType, "binary", StringComparison.OrdinalIgnoreCase);Assets/Infrastructure/Auth/JwtTokenParser.cs (1)
31-34
: 로깅 과다 가능성 및 민감 정보 노출 주의파싱 실패 시 매 요청마다 LogError는 스팸이 될 수 있습니다. 디버그 빌드에서만 상세 로그를 남기거나, 샘플링/레이트 리밋을 고려하세요.
Assets/Infrastructure/Auth/OAuth2/Handlers/IOAuth2CallbackHandler.cs (2)
17-24
: 콜백 대기 API의 Null 가능성 명시 및 취소 지원 제안WaitForCallbackAsync가 "없으면 null"을 반환한다고 문서화되었으나 형식은 Task입니다. string?로 명시하는 편이 안전하고, 취소/타임아웃 제어를 위해 CancellationToken도 고려해주세요.
- Task<string> WaitForCallbackAsync(); + Task<string?> WaitForCallbackAsync(System.Threading.CancellationToken cancellationToken = default);구현 클래스들(WebGL/Mobile/Desktop) 시그니처 업데이트 필요 시 말씀 주세요. 적용 패치 제공 가능합니다.
25-33
: IDisposable 도입 고려Cleanup 패턴을 강제하려면 IDisposable 채택이 명시적입니다. using 패턴으로 자원 해제를 보장할 수 있습니다.
Assets/Core/DebugConsole/GameDebugConsoleManager.cs (3)
154-156
: 자기 로그 필터의 문화권·대소문 영향"[DEBUG_CONSOLE]" / "[TEST]" 필터는 Contains 기본 비교를 사용합니다. Ordinal 비교로 고정해 의도치 않은 누락/통과를 방지하세요.
- if (logString.Contains("[DEBUG_CONSOLE]") || logString.Contains("[TEST]")) + if (logString.Contains("[DEBUG_CONSOLE]", StringComparison.Ordinal) || + logString.Contains("[TEST]", StringComparison.Ordinal)) return;
546-573
: GetPersistentEventCount 해석 주의UnityEvent의 GetPersistentEventCount는 인스펙터에 바인딩된 이벤트만 집계하고, 코드로 AddListener 한 런타임 리스너는 포함하지 않습니다. 진단 로그 문구에 이 점을 명시하거나, 실제 토글 동작 여부를 함께 출력하세요.
119-149
: 디버그 로그 과다 가능성설정 또는 Debug.isDebugBuild로 래핑해 로그 스팸을 줄이세요(모바일 성능 영향).
- Debug.Log("[DEBUG_CONSOLE] SetupUI called"); + if (Debug.isDebugBuild) Debug.Log("[DEBUG_CONSOLE] SetupUI called"); ... - Debug.Log($"[DEBUG_CONSOLE] Console panel set to: {_isConsoleVisible}"); + if (Debug.isDebugBuild) Debug.Log($"[DEBUG_CONSOLE] Console panel set to: {_isConsoleVisible}");Also applies to: 360-374
Assets/Infrastructure/Network/DTOs/Auth/GuestLoginRequest.cs (2)
22-26
: 생성자에서 공백문자만 입력되는 경우 허용null은 막지만 공백만 있는 경우 객체가 생성됩니다. DTO 레벨에서 바로 거부하면 오류 전파가 명확합니다.
- public GuestLoginRequest(string guestId) - { - GuestId = guestId ?? throw new ArgumentNullException(nameof(guestId)); - } + public GuestLoginRequest(string guestId) + { + if (guestId is null) throw new ArgumentNullException(nameof(guestId)); + if (string.IsNullOrWhiteSpace(guestId)) throw new ArgumentException("guestId cannot be empty or whitespace.", nameof(guestId)); + GuestId = guestId; + }
15-17
: 스키마 엄격화 제안게스트 ID는 필수 값이므로 JSON 스키마 수준에서도 Required를 명시하면 조기 검증에 유리합니다.
- [JsonProperty("guestId")] + [JsonProperty("guestId", Required = Required.Always)] public string GuestId { get; set; }Assets/Infrastructure/Auth/OAuth2/Models/PKCEParameters.cs (2)
31-34
: CreatedAt 직렬화 안전성 개선 제안(DateTimeKind/역직렬화 케이스)일부 직렬화기에서 DateTime.Kind가 Unspecified로 들어오면 비교 시 미묘한 버그가 날 수 있습니다. DateTimeOffset 사용 또는 setter에서 Utc로 강제 지정하는 방식을 고려해 주세요.
예시(선호): CreatedAt을 DateTimeOffset으로 변경.
대안: CreatedAt set 시CreatedAt = DateTime.SpecifyKind(value, DateTimeKind.Utc);
Also applies to: 36-42
14-24
: 불변성 고려(선택): 코드 파라미터 변경 억제PKCE 파라미터는 생성 후 변경될 이유가 거의 없습니다. 가능하면 init 접근자 또는 private set으로 제한해 실수 변경을 막는 것을 권장합니다. 단, JSON 역직렬화 제약을 확인 후 적용하세요.
Packages/packages-lock.json (1)
170-179
: com.unity.connect.share 도입 영향 점검해당 패키지는 편의성은 높지만, 에디터 의존/배포물 영향(크기/권한) 이슈가 있을 수 있습니다. 실제 런타임 필요 여부를 재확인하고, 개발용이라면 Dev 전용 프로필로 제한하는 방안을 검토하세요.
Assets/Core/Attribute/ReadOnlyDrawer.cs.meta (1)
1-2
: PropertyDrawer의 Editor 전용 컴파일 보장 확인.Drawer는 Editor 전용이어야 합니다. 현재 경로가 Editor 폴더 하위가 아니므로, asmdef로 includePlatforms=Editor 설정이 없다면 런타임 빌드 포함 위험이 있습니다. 경로나 asmdef를 통해 Editor 전용임을 보장해 주세요.
필요 시 다음 중 하나를 적용:
- 파일을 Assets/Core/Attribute/Editor/로 이동
- 해당 폴더에 Editor 전용 asmdef 추가(이미 있다면 Drawer 파일을 그 asmdef 범위로 이동)
Packages/manifest.json (1)
8-8
: Editor 전용 패키지 사용 여부 재검토com.unity.connect.share(4.2.3)은 주로 에디터 워크플로우용입니다. 런타임 의존이 없다면 제거하여 의존성/업데이트 표면을 줄이는 것을 권장합니다.
원하시면 사용처 스캔 스크립트를 드리겠습니다(메뉴 아이템/ASMDEF 의존 포함).
Assets/Infrastructure/Network/Services/STTService.cs (1)
68-68
: 엔드포인트 절대 경로 및 인증 비요구 호출 의도 확인 필요
"/api/v1/stt/transcribe"
절대 경로 사용으로 URL 조합 규칙이 바뀐 것(베이스 URL + 절대 경로 허용)이 맞는지 확인 바랍니다. 이전 상대 경로 대비 이슈 없으면 OK.requiresAuth: false
가 서버 정책과 부합하는지 확인이 필요합니다. 인증이 필요한 환경에서는 401/403이 발생할 수 있습니다.또한, 현재 하드코딩된 언어(
forcedLanguage = "ko"
)로 인해 메서드 매개변수language
가 반영되지 않습니다. 서버 요구사항이 고정 “ko”가 아니라면 매개변수 우선 사용을 고려해주세요.Also applies to: 70-70
Assets/Infrastructure/Network/WebSocket/WebSocketFactory.cs (1)
37-40
: WebGL 경로 추가는 타당. 에디터 플레이모드에서의 WebGL 경로 검토 권장
- WebGL 분기 추가로 런타임 플랫폼 매핑이 명확해졌습니다.
- 다만
Application.isEditor
에서 무조건 Desktop을 반환하므로, 에디터 플레이모드에서 WebGL 경로를 검증하려면 별도 강제 팩토리(CreateWebSocket(WebSocketType.WebGL)
)를 사용해야 합니다. 이 흐름이 팀의 테스트 전략과 맞는지 확인 바랍니다. 필요 시 에디터에서도 빌드 타깃/디파인 기준으로 WebGL 구현을 선택하는 옵션을 고려할 수 있습니다.Also applies to: 58-60, 72-74
Assets/Infrastructure/Network/Services/CharacterApiService.cs (1)
38-38
: API 버전 경로로의 일괄 전환은 일관성 측면에서 👍 — 라우트 상수화 제안
"/api/v1/character"
로의 전환은 명확하고 일관적입니다.- 후속 유지보수를 위해 버전/리소스 경로를 상수/라우트 헬퍼로 중앙집중화하면 변경 비용을 더 줄일 수 있습니다. 서버 측도 동일 버전으로 배포되었는지 최종 확인 부탁드립니다.
Also applies to: 55-55, 66-66, 105-105, 146-146
Assets/Core/Attribute/ReadOnlyDrawer.cs (2)
8-13
: GUI.enabled 전역 토글 대신 DisabledScope/BeginProperty 사용 권장상태 누수 및 프리팹 오버라이드 하이라이트 보전을 위해 DisabledScope + Begin/EndProperty 패턴이 더 안전합니다.
아래처럼 교체를 제안합니다:
- public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - GUI.enabled = false; - EditorGUI.PropertyField(position, property, label, true); - GUI.enabled = true; - } + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + using (new EditorGUI.DisabledScope(true)) + { + EditorGUI.PropertyField(position, property, label, includeChildren: true); + } + EditorGUI.EndProperty(); + }
5-14
: 네임스페이스 누락 — 사내 컨벤션 준수 필요폴더 구조 기반 네임스페이스(
ProjectVG.Core.Attribute
) 적용이 권장됩니다.아래와 같이 감싸주세요:
#if UNITY_EDITOR using UnityEngine; using UnityEditor; -[CustomPropertyDrawer(typeof(ReadOnlyAttribute))] -public class ReadOnlyDrawer : PropertyDrawer -{ +namespace ProjectVG.Core.Attribute +{ + [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] + public class ReadOnlyDrawer : PropertyDrawer + { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); using (new EditorGUI.DisabledScope(true)) { EditorGUI.PropertyField(position, property, label, includeChildren: true); } EditorGUI.EndProperty(); } -} +} #endifAssets/Domain/Chat/Service/ChatSystemManager.cs (4)
29-29
: 기본 CharacterId 상수 값 재검증 필요현재 기본값은 GUID 형태이나 CharacterManager는 초기 로드에 "zero"를 사용합니다. 서버/콘텐츠와 불일치 가능성이 있으니 통일 혹은 동기화가 필요합니다.
124-129
: public async void → UniTask/Task로 전환API 호출 실패 전파/테스트 용이성을 위해 async void 지양이 권장됩니다. (UI 이벤트 핸들러가 아니라면 특히)
아래 변경을 제안합니다:
- public async void SendUserMessage(string message) + public async Cysharp.Threading.Tasks.UniTask SendUserMessage(string message) { if (!ValidateUserInput(message)) { return; } try { // ... - if (_chatApiService != null) { + if (_chatApiService != null) { // ... - var response = await _chatApiService.SendChatAsync(chatRequest); + var response = await _chatApiService.SendChatAsync(chatRequest); // 지원 시 CancellationToken 전달 고려 } // ...Awake/Start 등 호출부가 있다면
await
혹은.Forget()
로 전환 필요합니다.
139-147
: 타임스탬프는 UTC 사용 권장서버/로그 일관성 확보를 위해 RequestAt에 UtcNow 사용이 안전합니다.
- RequestAt = DateTime.Now + RequestAt = DateTime.UtcNow
139-147
: CharacterId 소스 단일화현재 하드코딩된
_characterId
를 사용합니다. 실제 로드된 캐릭터와 불일치 시 서버 응답/연출이 꼬일 수 있습니다. CharacterManager에서 현재 캐릭터 ID를 노출받아 우선 사용하고, 없으면 폴백하세요.아래와 같은 접근을 권장합니다(참고용):
- CharacterId = _characterId, + CharacterId = ResolveCharacterId(),추가 메서드(파일 내 임의 위치):
private string ResolveCharacterId() { // CharacterManager에 제안한 CurrentCharacterId 속성 사용 return !string.IsNullOrEmpty(_characterManager?.CurrentCharacterId) ? _characterManager.CurrentCharacterId : _characterId; }또는 SystemManager 단계에서 공통 설정으로 주입하는 방식도 가능합니다.
Assets/Domain/Character/Script/CharacterManager.cs (2)
21-23
: 현재 캐릭터 ID 노출/동기화 추가 제안Chat과 서버 상호작용을 위해 현재 활성 캐릭터 ID를 추적·노출하면 상호 모듈 동기화가 쉬워집니다.
아래와 같이 필드/프로퍼티와 할당을 추가하세요:
private GameObject _currentCharacter; - private CharacterActionController _actionController; + private CharacterActionController _actionController; + private string _currentCharacterId; + public string CurrentCharacterId => _currentCharacterId;if (newCharacter != null) { _currentCharacter = newCharacter; _actionController = _currentCharacter.GetComponent<CharacterActionController>(); + _currentCharacterId = characterId; _currentCharacter.SetActive(true); Debug.Log($"[CharacterManager] 캐릭터 로드 완료: {characterId}"); }
if (_currentCharacter != null) { Destroy(_currentCharacter); _currentCharacter = null; _actionController = null; + _currentCharacterId = null; }
Also applies to: 89-95, 103-108
156-175
: 반환 타입과 널 반환의 불일치GetVoiceAudioSource는 null을 반환할 수 있으므로
AudioSource?
로 선언하거나, null 반환을 없애고 기본 AudioSource를 생성/바인딩하는 쪽이 안전합니다.예:
- private AudioSource GetVoiceAudioSource() + private AudioSource? GetVoiceAudioSource()또는 null 시
new GameObject("VoiceAudioSource").AddComponent<AudioSource>()
생성.Assets/Core/Managers/SystemManager.cs (1)
129-153
: 초기화 진입점의 async void 지양Initialize를 UniTask로 전환하고 호출부에서
.Forget()
또는await
하도록 하면 오류 전파/테스트가 용이합니다.- public async void Initialize() + public async Cysharp.Threading.Tasks.UniTask Initialize() { try { if (_initializationKickoffDone && IsInitialized) { return; } _initializationKickoffDone = true; // ... - await InitializeAppAsync(); + await InitializeAppAsync(); } catch (Exception ex) { // ... } }Awake 내 호출부(참고):
if (_autoInitializeOnStart && !_initializationKickoffDone && !IsInitialized) { _initializationKickoffDone = true; Initialize().Forget(); }Assets/Infrastructure/Auth/Models/RefreshToken.cs (2)
14-19
: expiresIn 유효성 체크 추가 권장음수/비정상 입력 방지를 위해 가드가 있으면 디버깅이 쉬워집니다.
- public RefreshToken(string token, int expiresIn, string deviceId = null) + public RefreshToken(string token, int expiresIn, string deviceId = null) { Token = token ?? throw new ArgumentNullException(nameof(token)); - ExpiresAt = DateTime.UtcNow.AddSeconds(expiresIn); + if (expiresIn < 0) throw new ArgumentOutOfRangeException(nameof(expiresIn)); + ExpiresAt = DateTime.UtcNow.AddSeconds(expiresIn); DeviceId = deviceId ?? string.Empty; }
28-31
: (선택) IsExpiringSoon 추가로 API 파리티 확보AccessToken과 동일한 프리만료 판정 API가 있으면 토큰 갱신 판단 로직 재사용이 수월합니다.
참고 구현:
public bool IsExpiringSoon(int minutesBeforeExpiry) { return DateTime.UtcNow.AddMinutes(minutesBeforeExpiry) >= ExpiresAt; }Assets/Infrastructure/Auth/Models/AccessToken.cs (2)
8-9
: 토큰/만료 속성은 불변으로외부에서 임의 변경되지 않도록 setter를 제한하세요.
- public string Token { get; set; } - public DateTime ExpiresAt { get; set; } + public string Token { get; private set; } + public DateTime ExpiresAt { get; private set; }
24-27
: IsExpiringSoon 파라미터 유효성음수 값으로 호출되면 즉시 true가 될 수 있습니다. 방어 로직을 넣어주세요.
- public bool IsExpiringSoon(int minutesBeforeExpiry) + public bool IsExpiringSoon(int minutesBeforeExpiry) { - return DateTime.UtcNow.AddMinutes(minutesBeforeExpiry) >= ExpiresAt; + if (minutesBeforeExpiry < 0) minutesBeforeExpiry = 0; + return DateTime.UtcNow.AddMinutes(minutesBeforeExpiry) >= ExpiresAt; }Assets/Infrastructure/Auth/OAuth2/IServerOAuth2Client.cs (3)
1-5
: 취소 토큰 사용을 위한 using 추가비동기 흐름 취소 지원을 위해 CancellationToken 네임스페이스를 import하세요.
using System; using System.Threading.Tasks; +using System.Threading; using ProjectVG.Infrastructure.Auth.Models; using ProjectVG.Infrastructure.Auth.OAuth2.Models;
21-41
: 장기 실행 API는 CancellationToken을 받도록 설계사용자 상호작용/콜백 대기 등은 취소가 필수입니다. 신규 인터페이스이므로 지금 바꾸는 것이 안전합니다.
- Task<PKCEParameters> GeneratePKCEAsync(); + Task<PKCEParameters> GeneratePKCEAsync(CancellationToken cancellationToken = default); ... - Task<string> StartServerOAuth2Async(PKCEParameters pkce); + Task<string> StartServerOAuth2Async(PKCEParameters pkce, CancellationToken cancellationToken = default); ... - Task<(bool success, string state)> HandleOAuth2CallbackAsync(string callbackUrl); + Task<(bool success, string state)> HandleOAuth2CallbackAsync(string callbackUrl, CancellationToken cancellationToken = default); ... - Task<TokenSet> RequestTokenAsync(string state); + Task<TokenSet> RequestTokenAsync(string state, CancellationToken cancellationToken = default); ... - Task<TokenSet> LoginWithServerOAuth2Async(); + Task<TokenSet> LoginWithServerOAuth2Async(CancellationToken cancellationToken = default);
31-31
: Tuple 반환 대신 명시적 타입 권장콜백 결과는 전용 DTO/struct(OAuth2CallbackResult 등)로 노출하면 가독성/확장성에 유리합니다. 필요 시 생성해드릴게요.
Assets/Domain/Character/Script/CharacterModelLoader.cs (5)
133-137
: 로그 메시지 오타 (컴포넌트 명 불일치)CubismMouthController를 추가하면서 AudioMouthInput을 추가했다고 로그가 나옵니다. 디버깅 혼선을 줄이기 위해 수정하세요.
- Debug.Log($"[CharacterModelLoader] CubismAudioMouthInput 컴포넌트를 추가했습니다: {modelInstance.name}"); + Debug.Log($"[CharacterModelLoader] CubismMouthController 컴포넌트를 추가했습니다: {modelInstance.name}");
60-72
: Animator 충돌 방지 강화Controller만 null로 두면 Animator.Update 오버헤드는 남습니다. 완전히 쓰지 않는다면 비활성화까지 권장합니다.
if (animator != null && animator.runtimeAnimatorController != null) { Debug.LogWarning($"[CharacterModelLoader] Animator Controller가 설정되어 있습니다. Live2D 호환성을 위해 제거합니다: {modelInstance.name}"); animator.runtimeAnimatorController = null; + animator.enabled = false; Debug.Log($"[CharacterModelLoader] Animator Controller 제거 완료: {modelInstance.name}"); }
118-121
: Action 초기화 시 Config 재탐색 제거(직접 전달)동일 메서드 체인에서 이미 가진 config를 재조회하면 스코프 의존이 늘고 오류 여지가 생깁니다. 인자로 넘기세요.
- SetupActionController(modelInstance); + SetupActionController(modelInstance, config);
246-271
: Action 초기화 로컬화 및 단순화_config 재조회 제거, null 방어 간소화 제안입니다.
- private void SetupActionController(GameObject modelInstance) + private void SetupActionController(GameObject modelInstance, Live2DModelConfig config) { var actionService = modelInstance.GetComponent<CharacterActionController>(); if (actionService == null) { actionService = modelInstance.AddComponent<CharacterActionController>(); } var motionController = modelInstance.GetComponent<CubismMotionController>(); if (motionController == null) { Debug.LogWarning($"[CharacterModelLoader] CubismMotionController를 찾을 수 없습니다: {modelInstance.name}"); return; } - // 현재 모델의 Config 찾기 - string modelId = modelInstance.name; - if (_modelRegistry != null && _modelRegistry.TryGetConfig(modelId, out var config)) - { - actionService.Initialize(motionController, config.MotionClips); - Debug.Log($"[CharacterModelLoader] CharacterActionController 초기화 완료: {modelInstance.name}, Motion Clips: {config.MotionClips?.Count ?? 0}개, Auto Idle: {config.EnableAutoIdle}"); - } - else - { - Debug.LogWarning($"[CharacterModelLoader] 모델 '{modelId}'의 Config를 찾을 수 없어 빈 Motion Clips로 초기화합니다."); - actionService.Initialize(motionController, new System.Collections.Generic.List<Live2DModelConfig.MotionClipMapping>()); - } + if (config != null) + { + actionService.Initialize(motionController, config.MotionClips); + Debug.Log($"[CharacterModelLoader] CharacterActionController 초기화 완료: {modelInstance.name}, Motion Clips: {config.MotionClips?.Count ?? 0}개, Auto Idle: {config.EnableAutoIdle}"); + } + else + { + Debug.LogWarning($"[CharacterModelLoader] 모델 '{modelInstance.name}'의 Config가 null입니다. 빈 Motion Clips로 초기화합니다."); + actionService.Initialize(motionController, new List<Live2DModelConfig.MotionClipMapping>()); + } }
140-154
: 립싱크 체인 연결 확인CubismAudioMouthInput이 CubismMouthController로 제대로 라우팅되는지(자동 바인딩/필드 지정) 라이브러리 버전에 따라 다릅니다. 수동 참조 설정이 필요하다면 누락 시 립싱크가 무효가 됩니다. 간단한 확인을 권장합니다. 필요 시 연결 코드 추가를 제안할 수 있습니다.
Assets/Infrastructure/Auth/Examples/GuestLoginExample.cs (3)
1-5
: DateTime 형식 사용을 위한 using 추가(선택)만료 시각을 포맷팅하려면 System 네임스페이스가 필요합니다.
using UnityEngine; using ProjectVG.Infrastructure.Auth.Services; using ProjectVG.Infrastructure.Auth.Utils; using ProjectVG.Infrastructure.Auth.Models; +using System;
152-156
: Null 안전 로그 및 UTC 표기AccessToken이 null이거나 만료 시각 Kind가 불분명할 수 있어 방어적으로 출력하세요.
- Debug.Log("[GuestLoginExample] Guest 로그인 성공 이벤트 수신"); - Debug.Log($"[GuestLoginExample] AccessToken 만료: {tokenSet.AccessToken.ExpiresAt}"); + Debug.Log("[GuestLoginExample] Guest 로그인 성공 이벤트 수신"); + var exp = tokenSet?.AccessToken?.ExpiresAt ?? DateTime.MinValue; + Debug.Log($"[GuestLoginExample] AccessToken 만료 시각(UTC): {exp.ToUniversalTime():yyyy-MM-dd HH:mm:ss 'UTC'}");
48-70
: async void 사용 범위 확인Start()에서 fire-and-forget 용도면 무방하나, 예외 전파가 안 됩니다. 로그 수집을 위해 try/catch로 감싸거나 UniTask.Void 패턴으로 전환을 고려하세요.
Assets/Infrastructure/Auth/OAuth2/Handlers/OAuth2CallbackHandlerFactory.cs (2)
43-58
: 플랫폼 이름 표준화 확인 (Config와 불일치 가능성)본 팩토리는 else에서 "Editor"를 반환하지만, ServerOAuth2Config.GetCurrentPlatformName()은 else에서 "Unknown"을 반환합니다(맥락 스니펫 참고). 소비자가 문자열 비교를 한다면 미묘한 버그가 생길 수 있어 통일을 권장합니다. 팩토리 쪽을 "Unknown"으로 맞추는 예:
#else - return "Editor"; + return "Unknown"; #endif혹은 Config 쪽을 "Editor"로 맞추는 방안도 있습니다. 어느 쪽을 기준으로 삼을지 결정해주세요.
64-68
: 지원 확인 시 불필요한 로그/할당 최소화IsPlatformSupported가 CreateHandler()를 호출해 핸들러 생성 및 로그를 발생시킵니다. 단순 지원 여부 확인이라면 전처리기 분기만으로 계산하는 경량 구현을 고려하세요. 필요 시 대안 구현을 제안할 수 있습니다.
Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
33-33
: 중복된 HttpClient 검증 제거 고려생성자에서 이미 검증했으므로 메서드 진입마다 재검증은 과합니다(싱글턴이 동적으로 바뀌지 않는다면). 유지 필요 이유가 없다면 제거 권장.
- ValidateHttpClient();
Assets/Infrastructure/Auth/Examples/AuthManagerExample.cs (1)
296-312
: _authManager null 가드 추가희박하지만 초기화 타이밍 이슈로 NRE가 날 수 있습니다. 안전가드 추가 권장.
private void SetButtonsEnabled(bool enabled) { - if (guestLoginButton != null) + if (_authManager == null) + return; + if (guestLoginButton != null) guestLoginButton.interactable = enabled && !_authManager.IsLoggedIn && _authManager.CanLoginAsGuest();Assets/Infrastructure/Auth/Utils/DeviceIdProvider.cs (3)
77-80
: 디바이스 ID에 타임스탬프 포함은 불필요한 변동성 유발재설치/초기화 시점이 노출되고 ID 안정성이 떨어집니다. 고정 식별이 목적이면 제거 권장.
- var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - return $"{platformPrefix}-{platformId}-{timestamp}"; + return $"{platformPrefix}-{platformId}";
125-130
: WebGL에서 GetHashCode 사용은 비결정적이며 런타임마다 달라질 수 있음안정 해시로 교체하세요(SHA-256 등). 아래처럼 구성 문자열을 안정 해시로 치환 권장.
- string userAgent = Application.platform.ToString(); - string screenInfo = $"{Screen.width}x{Screen.height}"; - return $"{browserId}-{userAgent.GetHashCode()}-{screenInfo.GetHashCode()}"; + string userAgent = Application.platform.ToString(); + string screenInfo = $"{Screen.width}x{Screen.height}"; + var composite = $"{browserId}|{userAgent}|{screenInfo}"; + return ComputeStableHash(composite);추가 헬퍼(파일 내 임의 위치):
private static string ComputeStableHash(string input) { try { using (var sha = System.Security.Cryptography.SHA256.Create()) { var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty)); return BitConverter.ToString(bytes, 0, 8).Replace("-", "").ToLowerInvariant(); // 8바이트(16hex)만 사용 } } catch { return (input ?? string.Empty).GetHashCode().ToString("x"); // 최후 fallback } }
139-147
: Windows 레지스트리 키 Dispose 누락핸들 누수 방지를 위해
using
사용 권장.- var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography"); - if (key != null) - { - var machineGuid = key.GetValue("MachineGuid")?.ToString(); - if (!string.IsNullOrEmpty(machineGuid)) - { - return machineGuid; - } - } + using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography")) + { + if (key != null) + { + var machineGuid = key.GetValue("MachineGuid")?.ToString(); + if (!string.IsNullOrEmpty(machineGuid)) + { + return machineGuid; + } + } + }Assets/Infrastructure/Auth/OAuth2/Handlers/MobileCallbackHandler.cs (4)
64-68
: 정리 시 이벤트 구독 해제 필요포커스 이벤트를 구독하는 경우 누수 방지를 위해 해제해야 합니다. 아래 Initialize 수정과 세트로 적용하세요.
public void Cleanup() { _isDisposed = true; + Application.focusChanged -= OnApplicationFocus; Debug.Log("[MobileCallbackHandler] 정리 완료"); }
23-31
: 포커스 이벤트 구독으로 콜백 체크 트리거 강화본 클래스는 MonoBehaviour가 아니므로 Unity 메시지 함수가 자동 호출되지 않습니다.
Application.focusChanged
를 구독해 신호를 받도록 하세요.public async Task InitializeAsync(string expectedState, float timeoutSeconds) { _expectedState = expectedState; _timeoutSeconds = timeoutSeconds; _isInitialized = true; + Application.focusChanged += OnApplicationFocus; Debug.Log($"[MobileCallbackHandler] 초기화 완료 - State: {expectedState}, Timeout: {timeoutSeconds}초"); await UniTask.CompletedTask; }
100-107
: 커스텀 스킴 URL 필터가 과도하게 제한적
auth/callback
포함 여부만으로 거르는 것은 취약/과잉 제한 모두 가능.state
파라미터 존재 여부 등으로 필터링하거나 공용 파서(OAuth2CallbackParser
)를 재사용하세요.- if (!string.IsNullOrEmpty(url) && url.Contains("auth/callback")) + if (!string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(ProjectVG.Infrastructure.Auth.OAuth2.Utils.OAuth2CallbackParser.ExtractParameter(url, "state"))) { - _lastCustomUrl = url; - Debug.Log($"[MobileCallbackHandler] 커스텀 스킴 URL 설정: {url}"); + _lastCustomUrl = url; + Debug.Log($"[MobileCallbackHandler] 커스텀 스킴 URL 설정: {SanitizeCallbackUrl(url)}"); }
109-120
: MonoBehaviour 아님 — Unity 자동 메시지 호출되지 않음
OnApplicationFocus/OnApplicationPause
는 자동으로 호출되지 않습니다. 이벤트 구독 방식으로 바꾸거나 별도 MonoBehaviour 브리지를 사용하세요(위 Initialize/Cleanup 수정 참조).Assets/Infrastructure/Network/DTOs/Chat/ChatRequest.cs (1)
3-3
: 불필요한 using 제거UnityEngine 네임스페이스가 사용되지 않습니다.
-using UnityEngine;
Assets/Infrastructure/Auth/Services/GuestAuthService.cs (2)
2-2
: 불필요한 using 제거System.Threading.Tasks는 사용되지 않습니다. UniTask만 사용 중입니다.
-using System.Threading.Tasks;
134-146
: CanLoginAsGuest가 부수효과로 ID를 생성합니다GetDeviceId()가 저장소에 없을 때 새 ID를 생성합니다. “가능 여부 확인” 단계에서 생성 의도가 아니라면 부수효과 없는 확인 메서드(예: TryGetStoredDeviceId)로 분리하는 것을 고려하세요.
Assets/Infrastructure/Auth/OAuth2/Utils/PKCEGenerator.cs (1)
22-41
: 길이 매개변수 사전 검증(조기 실패) 제안GeneratePKCEAsync에서 허용 범위 외 입력을 즉시 거부하면 후속 처리·로그 복잡도가 줄어듭니다.
public static async Task<PKCEParameters> GeneratePKCEAsync(int codeVerifierLength = 64, int stateLength = 16) { try { + if (codeVerifierLength < 43 || codeVerifierLength > 128) + throw new ArgumentOutOfRangeException(nameof(codeVerifierLength), "43-128 범위여야 합니다."); + if (stateLength < 16 || stateLength > 64) + throw new ArgumentOutOfRangeException(nameof(stateLength), "16-64 범위여야 합니다.");Assets/Infrastructure/Auth/OAuth2/Config/ServerOAuth2Config.cs (2)
101-142
: 리다이렉트 URI 형식 검증 추가문자열 비어있음 체크 외에 Uri.TryCreate로 형식을 검증하면 설정 실수를 조기 발견할 수 있습니다.
- // 기본 필수 값 검사 - var hasRequiredFields = !string.IsNullOrEmpty(GetCurrentPlatformRedirectUri()); + // 기본 필수 값 + URI 형식 검사 + var redirect = GetCurrentPlatformRedirectUri(); + var hasRequiredFields = !string.IsNullOrEmpty(redirect); + var hasValidRedirectUri = Uri.TryCreate(redirect, UriKind.Absolute, out _); ... - var isValid = hasRequiredFields && hasValidPKCE && hasValidTimeout; + var isValid = hasRequiredFields && hasValidRedirectUri && hasValidPKCE && hasValidTimeout; ... - if (string.IsNullOrEmpty(GetCurrentPlatformRedirectUri())) + if (string.IsNullOrEmpty(redirect)) Debug.LogError($" - redirectUri: 비어있음"); - else if (GetCurrentPlatformRedirectUri().Contains("your-domain")) + else if (redirect.Contains("your-domain")) Debug.LogError($" - redirectUri: '{GetCurrentPlatformRedirectUri()}' (기본값입니다. 실제 도메인으로 변경하세요)"); else - Debug.LogError($" - redirectUri: '{GetCurrentPlatformRedirectUri()}'"); + Debug.LogError($" - redirectUri: '{redirect}' (형식 유효: {hasValidRedirectUri})");
147-162
: 플랫폼명 결정 로직 중복 제거 제안동일 로직이 OAuth2CallbackHandlerFactory.GetCurrentPlatformName에도 존재합니다. 한 곳으로 공통화해 불일치 리스크를 낮추세요.
Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs (2)
34-41
: 미사용/형식적 로직 정리
_cancellationTokenSource
는 생성만 하고 사용하지 않습니다.CheckForCallbackUrl()
은 아무 동작을 하지 않습니다.불필요 멤버/메서드를 제거하거나, CTS를
GetContextAsync
취소에 실제로 연결하세요.Also applies to: 265-296
131-136
: 리스닝 태스크 예외 처리 및 백프레셔
_ = ProcessCallbackAsync(context);
는 파이어-앤-포겟으로 예외 전파가 되지 않습니다. 처리 중 예외가 누락될 수 있으니 Try/Catch 래퍼 또는 태스크 수집/로깅을 권장합니다.- var context = await _listener.GetContextAsync(); - _ = ProcessCallbackAsync(context); + var context = await _listener.GetContextAsync(); + _ = ProcessCallbackAsync(context).ContinueWith(t => + { + if (t.IsFaulted) Debug.LogError($"[DesktopCallbackHandler] 처리 태스크 예외: {t.Exception?.GetBaseException().Message}"); + });Assets/Infrastructure/Auth/TokenRefreshService.cs (1)
79-84
: 동시 갱신 가드 강화 제안bool 플래그만으로는 경합 가능성이 있습니다. Interlocked 기반 정수 플래그로 전환을 권장합니다.
예시:
private int _refreshingFlag = 0; // 시작 시: if (Interlocked.Exchange(ref _refreshingFlag, 1) == 1) return false; // finally: Interlocked.Exchange(ref _refreshingFlag, 0);Also applies to: 92-93, 141-142
Assets/Infrastructure/Auth/OAuth2/ServerOAuth2Provider.cs (2)
393-401
: WebGL에서 ExternalEval 사용 회피
Application.ExternalEval
은 더 이상 권장되지 않습니다.Application.OpenURL(authUrl)
또는 JSlib 브리지로 대체하세요.최신 Unity(WebGL)에서
ExternalEval
지원 여부를 확인해 주세요. 필요 시 대체 API로 교체 권장.
25-33
: 사용되지 않는 readonly 필드
_currentPKCE
는 항상 null로 고정되고 사용되지 않습니다. 제거하여 혼동을 줄이세요.- private readonly PKCEParameters _currentPKCE; @@ - _currentPKCE = null;Assets/Infrastructure/Auth/Examples/ServerOAuth2Example.cs (1)
97-107
: HttpApiClient 초기화 보조예제에서
IsInitialized
를 요구하지만 미초기화 시 자동 초기화를 시도하도록 개선하면 사용성이 좋아집니다.- if (!httpClient.IsInitialized) - { - Debug.LogError("[ServerOAuth2Example] HttpApiClient가 초기화되지 않았습니다."); - ShowStatus("HttpApiClient가 초기화되지 않았습니다.", Color.red); - return; - } + if (!httpClient.IsInitialized) + { + httpClient.Initialize(); + Debug.Log("[ServerOAuth2Example] HttpApiClient를 초기화했습니다."); + }Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs (1)
259-270
: 쿼리 문자열 연결 시 '?'/'&' 처리기존 구현은 항상 '?'를 사용합니다. 이미 쿼리가 있는 경우 '&'가 필요합니다. 위의 수정안 포함.
Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs (2)
185-208
: Dispose에서 비동기 Close 미대기 및 예외 누락Dispose에서는 대기할 수 없으므로 Close의 예외를 무시하고 확실히 해제하세요.
- if (_webSocket.State == WebSocketState.Open) - { - _webSocket.Close(); - } + try { if (_webSocket.State == WebSocketState.Open) _ = _webSocket.Close(); } + catch { /* ignored in Dispose */ } + _webSocket.Dispose(); _webSocket = null;
141-152
: 과도한 로그 출력메시지 본문과 바이트 수를 매 수신마다 상세 로그로 남기면 WebGL에서 성능/메모리 압박이 큽니다. 로그 레벨 가드(예: 개발 빌드 전용) 또는 샘플링을 권장합니다.
Assets/Domain/Character/Script/CharacterActionController.cs (3)
389-397
: 주석(0.3초 지연)과 구현(0.0초 지연)이 불일치합니다즉시 재귀 호출되어 Idle 모션이 프레임 경계만 두고 연속 재생될 수 있습니다.
- yield return new WaitForSeconds(0.0f); + yield return new WaitForSeconds(0.3f);또는 Live2DModelConfig.AutoIdleInterval을 주입받아 사용하도록 개선하세요.
10-11
: 불필요한 using 제거System.Runtime.CompilerServices는 사용되지 않습니다.
-using System.Runtime.CompilerServices;
334-384
: 설정값(endBehavior/AutoIdle)을 활용하면 유연성이 증가합니다MotionClipMapping.endBehavior와 Config의 AutoIdleInterval이 정의되어 있으나 현재 컨트롤러에서 활용되지 않습니다. 클립별 종료 동작(Stop/Loop/ReturnToIdle)과 Idle 간격을 설정 기반으로 구동하도록 확장하는 것을 권장합니다.
Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs (2)
221-251
: IsCustomScheme가 file/ftp 등 비-HTTP 스킴도 ‘커스텀’으로 분류합니다원치 않는 참값을 반환할 수 있습니다. expectedScheme가 없을 때는 http/https/file/ftp/mailto 등 일반 스킴을 제외하거나, expectedScheme 제공을 강제하는 것이 안전합니다.
- // 스킴이 http, https가 아니면 커스텀 스킴으로 간주 - if (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || - uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) - { - return false; - } + // 일반적으로 알려진 스킴은 제외 + var s = uri.Scheme.ToLowerInvariant(); + if (s is "http" or "https" or "file" or "ftp" or "mailto") + return false;
88-162
: 중복 로직 통합 제안ParseCallbackUrl/ParseSchemeUrl의 파라미터 처리 로직이 중복됩니다. 내부 헬퍼(공통: NameValueCollection→Dictionary 변환 및 success/state 검증)로 통합하세요.
Assets/Infrastructure/Auth/AuthManager.cs (1)
307-341
: EnsureValidTokenAsync가 항상 리프레시를 시도해 불필요한 네트워크 호출이 발생할 수 있습니다만료 임박 여부를 체크하는 기존 로직을 활성화하면 효율적입니다.
- /* - var currentToken = _tokenManager.GetAccessToken(); - if (IsLoggedIn && currentToken != null && !currentToken.IsExpiringSoon(minutesBeforeExpiry)) - { - return true; - } - */ + var currentToken = GetCurrentAccessToken(); + if (IsLoggedIn && currentToken != null && !currentToken.IsExpiringSoon(minutesBeforeExpiry)) + { + return true; + }Assets/Domain/Character/Script/Config/Live2DModelConfig.cs (2)
37-38
: 오탈자: #region Fild Auto → Field Auto인스펙터/문서 일관성을 위해 정정하세요.
- #region Fild Auto + #region Field Auto
240-252
: MotionClips OnValidate 연쇄 검증 좋습니다클립 변경 시 Id/MotionGroup 자동 동기화가 안정적입니다. CharacterActionController에서 endBehavior/AutoIdleInterval을 활용하도록 연계하면 설정 주도의 동작이 완성됩니다.
Assets/Infrastructure/Auth/OAuth2/Models/ServerOAuth2Models.cs (2)
22-27
: PKCE: authorize 요청에 code_verifier 포함 여부 확인 필요표준 PKCE 플로우에서는 authorize 단계에
code_challenge
만 전달하고code_verifier
는 토큰 교환 단계에서 사용합니다. 서버 계약이 authorize 단계의code_verifier
를 요구한다면 유지, 아니라면 제거가 바람직합니다.원하시면 서버 계약서/스펙 기준으로 정리해드릴게요.
168-187
: OAuth2BrowserResult.ErrorResult 시그니처 일관성 개선(선택)성공/실패 팩토리의 파라미터 구성이 다릅니다. 실패에도
browserType
을 받을 수 있게 하면 호출부가 단순해집니다.- public static OAuth2BrowserResult ErrorResult(string error, string platform) + public static OAuth2BrowserResult ErrorResult(string error, string platform, string browserType = null) { return new OAuth2BrowserResult { Success = false, Error = error, - Platform = platform + Platform = platform, + BrowserType = browserType }; }Assets/Infrastructure/Auth/TokenManager.cs (1)
294-307
: 시작 시 토큰 자동 복구 코루틴 오류 경로 누락
refreshTask.Status
가 Faulted/Cancelled인 경우 처리 로그가 없습니다. 실패 케이스를 명시적으로 로깅해 원인 파악을 돕는 것이 좋습니다.if (refreshTask.Status == Cysharp.Threading.Tasks.UniTaskStatus.Succeeded) { bool success = refreshTask.GetAwaiter().GetResult(); if (!success) { Debug.LogWarning("[TokenManager] 앱 시작 시 AccessToken 자동 복구 실패"); } } + else + { + Debug.LogWarning($"[TokenManager] 앱 시작 시 AccessToken 자동 복구 완료되지 않음 - 상태: {refreshTask.Status}"); + }Assets/Infrastructure/Network/Http/HttpApiClient.cs (3)
354-367
: 401/403 응답 시 자동 토큰 갱신 후 1회 재시도(선택)인증이 필요한 요청에서 Access Token 만료 시 첫 요청이 실패합니다. 401/403에서 Refresh 토큰이 유효하면 자동 갱신 후 1회 재시도를 권장합니다.
private async UniTask HandleRequestFailure(UnityWebRequest request, int attempt, CancellationToken cancellationToken, bool isFileUpload = false) { var error = new ApiException(request.error, request.responseCode, request.downloadHandler?.text); var requestType = isFileUpload ? "파일 업로드" : "API 요청"; + // 인증 실패 시 Refresh Token으로 1회 자동 갱신 시도 + if ((request.responseCode == 401 || request.responseCode == 403)) + { + try + { + var tm = ProjectVG.Infrastructure.Auth.TokenManager.Instance; + if (tm != null && tm.HasRefreshToken && !tm.IsRefreshTokenExpired()) + { + Debug.LogWarning("[HttpApiClient] 인증 실패 감지 — 토큰 갱신 시도"); + var refreshed = await ProjectVG.Infrastructure.Auth.TokenRefreshService.Instance.RefreshAccessTokenAsync(); + if (refreshed) + { + var newToken = tm.GetAccessToken(); + if (!string.IsNullOrEmpty(newToken)) + { + SetAuthToken(newToken); + await DelayForRetry(attempt, cancellationToken); + return; + } + } + } + } + catch (Exception ex) + { + Debug.LogError($"[HttpApiClient] 자동 토큰 갱신 실패: {ex.Message}"); + } + } + if (ShouldRetry(request.responseCode) && attempt < NetworkConfig.MaxRetryCount) { Debug.LogWarning($"[HttpApiClient] {requestType} 실패 (시도 {attempt + 1}/{NetworkConfig.MaxRetryCount + 1}): {error.Message}"); await DelayForRetry(attempt, cancellationToken); return; } throw error; }이 변경을 사용하려면 파일 상단에
using ProjectVG.Infrastructure.Auth;
추가가 필요합니다(아래 주석 참조).
1-12
: 네임스페이스 의존성 추가(선택, 401 처리 사용 시)401 처리 변경을 사용할 경우 TokenManager/TokenRefreshService 참조를 위해 Auth 네임스페이스 using이 필요합니다.
using Newtonsoft.Json; +using ProjectVG.Infrastructure.Auth;
248-251
: URL 판별 시 대소문자 무시(미세 개선)
StartsWith
에StringComparison.OrdinalIgnoreCase
를 지정하면 안전합니다.- return url.StartsWith("http://") || url.StartsWith("https://"); + return url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
fileFormatVersion: 2 | ||
guid: 1086dd3147594bf4d852540670558430 No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
에디터 Drawer/Attribute 스크립트의 .meta 누락 항목 확인
GUID만 있는 .meta는 참조 안정성이 떨어집니다. Unity 에디터에서 .meta를 재생성하고, 변경된 .meta 전부를 커밋해 주세요. Drawer 쪽 .meta도 함께 점검 부탁드립니다.
🏁 Script executed:
#!/bin/bash
# Fallback: list .meta files under Assets/Core and show first 10 lines of each
find Assets/Core -name "*.meta" -print -exec sed -n "1,10p" {} \;
Length of output: 4503
.meta 파일 재생성 및 커밋 필요
- Assets/Core/Attribute/ReadOnlyAttribute.cs.meta와 ReadOnlyDrawer.cs.meta(및 GUID만 포함된 다른 스크립트 .meta)에서
timeCreated
등 기본 필드가 누락되어 있습니다. Unity 에디터에서 해당 .cs 파일의 .meta를 재생성하고, 변경된 모든 .meta 파일을 커밋해주세요.
🤖 Prompt for AI Agents
Assets/Core/Attribute/ReadOnlyAttribute.cs.meta lines 1-2: 해당 .meta에서
timeCreated 등 기본 메타 필드가 누락되어 있어 Unity에서 재생성해야 합니다; Unity 에디터에서
Assets/Core/Attribute/ReadOnlyAttribute.cs 및
Assets/Core/Attribute/ReadOnlyDrawer.cs(및 GUID만 있는 다른 스크립트 .meta가 있는 파일들)를 열거나
해당 .cs 파일들을 우클릭 → Reimport 하거나 기존 .meta를 삭제한 뒤 에디터를 재시작하여 .meta를 재생성하고 변경된 모든
.meta 파일(timeCreated, licenseType, userData 등 기본 필드가 포함되었는지 확인)을 커밋하세요.
_consolePanel.SetActive(true); | ||
_isConsoleVisible = true; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
콘솔이 기본 표시 상태로 시작합니다 (빌드 노출 위험)
InitializeConsole에서 항상 패널을 활성화합니다. 실서비스 빌드 노출/성능 영향이 큽니다. 디버그 빌드에서만 기본 표시하거나 설정 플래그로 제어하세요.
- _consolePanel.SetActive(true);
- _isConsoleVisible = true;
+ var startVisible = Debug.isDebugBuild; // 또는 _settings?.StartVisible == true
+ _consolePanel.SetActive(startVisible);
+ _isConsoleVisible = startVisible;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
_consolePanel.SetActive(true); | |
_isConsoleVisible = true; | |
var startVisible = Debug.isDebugBuild; // 또는 _settings?.StartVisible == true | |
_consolePanel.SetActive(startVisible); | |
_isConsoleVisible = startVisible; |
🤖 Prompt for AI Agents
In Assets/Core/DebugConsole/GameDebugConsoleManager.cs around lines 106 to 108,
the InitializeConsole method always activates the console panel causing the
console to be visible by default in builds; change this to only enable the panel
in editor/development builds or when an explicit debug flag is set. Modify the
code so _consolePanel.SetActive(true) and _isConsoleVisible = true are executed
only under a development condition (e.g., wrap with #if UNITY_EDITOR ||
DEVELOPMENT_BUILD or check a serialized/Config debug boolean), default the flag
to false for production, and ensure the setting can be overridden via
inspector/launch argument so production builds do not expose the console by
default.
@@ -1,4 +1,5 @@ | |||
#nullable enable | |||
#if !UNITY_WEBGL || UNITY_EDITOR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
WebGL에서 클래스 전체 배제 → 씬/프리팹 Missing Script 리스크 점검 필요
파일 레벨 가드로 WebGL 빌드(에디터 제외)에서 이 MonoBehaviour가 완전히 빠지므로, 해당 컴포넌트를 참조하는 씬/프리팹이 있으면 빌드 시 Missing Script가 될 수 있습니다. WebGL용 씬 분기/프리팹 변형 또는 동일한 API의 No-Op 스텁 컴포넌트를(UNITY_WEBGL && !UNITY_EDITOR)로 따로 두는 방식을 검토해 주세요.
다음 스크립트로 프로젝트 내 참조를 빠르게 확인할 수 있습니다:
🏁 Script executed:
#!/bin/bash
# VoiceInputView가 연결된 씬/프리팹/에셋 검색
rg -n -S --glob 'Assets/**/*.{prefab,unity,asset}' '\bVoiceInputView\b' -C2
Length of output: 77
메타 파일의 GUID를 기반으로 씬/프리팹에서 실제 참조 여부를 재검증합니다.
🏁 Script executed:
#!/bin/bash
# VoiceInputView.cs.meta에서 GUID 추출 및 씬/프리팹 참조 검색
guid=$(grep -m1 '^guid:' Assets/Domain/Chat/View/VoiceInputView.cs.meta | awk '{print $2}')
echo "GUID=$guid"
rg -n "$guid" -S --glob 'Assets/**/*.{prefab,unity}' -C2
Length of output: 1026
WebGL 빌드 시 Missing Script 위험 – 스텁 컴포넌트 또는 씬 분기 도입 필요
VoiceInputView가 WebGL(에디터 제외)에서 완전히 제외되어 아래 씬에서 Missing Script가 발생합니다:
- Assets/Tests/Sences/DialogueSystem.unity
- Assets/App/Scenes/MainScene.unity
해결 방안:
- UNITY_WEBGL && !UNITY_EDITOR 조건으로 No-Op 스텁 MonoBehaviour 추가
- WebGL 전용 씬/프리팹 분기 처리
private async UniTaskVoid StartAutoTokenRefresh() | ||
{ | ||
while (_isLoggedIn && _currentTokenSet?.HasRefreshToken() == true) | ||
{ | ||
try | ||
{ | ||
// 토큰 갱신이 필요한지 확인 | ||
if (_currentTokenSet.NeedsRefresh()) | ||
{ | ||
Debug.Log("[ServerOAuth2Example] 토큰 갱신 시작"); | ||
|
||
// 현재는 전체 재로그인 플로우 실행 | ||
var newTokenSet = await _oauth2Provider.LoginWithServerOAuth2Async(); | ||
|
||
if (newTokenSet?.HasRefreshToken() == true) | ||
{ | ||
_currentTokenSet = newTokenSet; | ||
Debug.Log("[ServerOAuth2Example] 토큰 갱신 성공"); | ||
DisplayTokenInfo(); | ||
} | ||
else | ||
{ | ||
Debug.LogWarning("[ServerOAuth2Example] 토큰 갱신 실패"); | ||
break; | ||
} | ||
} | ||
|
||
// 1분 대기 | ||
await UniTask.Delay(60000); | ||
} | ||
catch (System.Exception ex) | ||
{ | ||
Debug.LogError($"[ServerOAuth2Example] 토큰 갱신 오류: {ex.Message}"); | ||
break; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
자동 갱신 시 전체 재로그인 대신 Refresh API 사용
지금은 만료 임박 시 브라우저 플로우를 다시 실행합니다. 사용자 경험/부하 측면에서 Refresh Token 갱신을 먼저 시도하세요.
- if (_currentTokenSet.NeedsRefresh())
- {
- Debug.Log("[ServerOAuth2Example] 토큰 갱신 시작");
-
- // 현재는 전체 재로그인 플로우 실행
- var newTokenSet = await _oauth2Provider.LoginWithServerOAuth2Async();
-
- if (newTokenSet?.HasRefreshToken() == true)
- {
- _currentTokenSet = newTokenSet;
- Debug.Log("[ServerOAuth2Example] 토큰 갱신 성공");
- DisplayTokenInfo();
- }
- else
- {
- Debug.LogWarning("[ServerOAuth2Example] 토큰 갱신 실패");
- break;
- }
- }
+ if (_currentTokenSet.NeedsRefresh())
+ {
+ Debug.Log("[ServerOAuth2Example] 토큰 자동 갱신 시도");
+ var ok = await _refreshService.RefreshAccessTokenAsync();
+ if (ok)
+ {
+ _currentTokenSet = _tokenManager.LoadTokens();
+ Debug.Log("[ServerOAuth2Example] 토큰 자동 갱신 성공");
+ DisplayTokenInfo();
+ }
+ else
+ {
+ Debug.LogWarning("[ServerOAuth2Example] 토큰 자동 갱신 실패");
+ break;
+ }
+ }
Committable suggestion skipped: line range outside the PR's diff.
Debug.Log("=== 🎉 OAuth2 로그인 성공 - 토큰 정보 ==="); | ||
|
||
// Access Token 정보 | ||
if (_currentTokenSet.AccessToken != null) | ||
{ | ||
Debug.Log($"✅ Access Token: {_currentTokenSet.AccessToken.Token}"); | ||
Debug.Log($" - 만료 시간: {_currentTokenSet.AccessToken.ExpiresAt:yyyy-MM-dd HH:mm:ss}"); | ||
Debug.Log($" - 만료까지: {(_currentTokenSet.AccessToken.ExpiresAt - DateTime.UtcNow).TotalMinutes:F1}분"); | ||
} | ||
else | ||
{ | ||
Debug.LogError("❌ Access Token이 null입니다."); | ||
} | ||
|
||
// Refresh Token 정보 | ||
if (_currentTokenSet.RefreshToken != null) | ||
{ | ||
Debug.Log($"✅ Refresh Token: {_currentTokenSet.RefreshToken.Token}"); | ||
Debug.Log($" - 만료 시간: {_currentTokenSet.RefreshToken.ExpiresAt:yyyy-MM-dd HH:mm:ss}"); | ||
Debug.Log($" - 만료까지: {(_currentTokenSet.RefreshToken.ExpiresAt - DateTime.UtcNow).TotalDays:F1}일"); | ||
Debug.Log($" - 디바이스 ID: {_currentTokenSet.RefreshToken.DeviceId}"); | ||
} | ||
else | ||
{ | ||
Debug.LogWarning("⚠️ Refresh Token이 null입니다."); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰 값 전체/부분 로그 노출 제거
액세스/리프레시 토큰 및 만료/디바이스 정보를 그대로 출력합니다. 민감정보 노출 위험이 큽니다. 마스킹 또는 길이만 로그로 남기세요. UI 표기도 동일.
- Debug.Log($"✅ Access Token: {_currentTokenSet.AccessToken.Token}");
+ Debug.Log($"✅ Access Token 수신 (len={_currentTokenSet.AccessToken.Token?.Length ?? 0})");
@@
- Debug.Log($"✅ Refresh Token: {_currentTokenSet.RefreshToken.Token}");
+ Debug.Log("✅ Refresh Token 수신");
- info += $"Access Token: {_currentTokenSet.AccessToken?.Token?.Substring(0, Math.Min(20, _currentTokenSet.AccessToken.Token.Length))}...\n";
+ info += $"Access Token: (len={_currentTokenSet.AccessToken?.Token?.Length ?? 0})\n";
Also applies to: 451-455
🤖 Prompt for AI Agents
In Assets/Infrastructure/Auth/Examples/ServerOAuth2Example.cs around lines
451-497, the code currently logs full access/refresh token strings and detailed
sensitive metadata; change the logging to avoid exposing sensitive data by
masking token values (e.g., show first 4 and last 4 chars with ellipsis or just
token length), and only log non-sensitive metadata or summaries (e.g., time
until expiry in minutes/hours/days and DeviceId only if necessary but
masked/hashed); update both the access token block (451-455 and 471-482) and the
refresh token block (483-497) to output masked tokens or token lengths and
remove raw token strings and any other sensitive plaintext.
Console.WriteLine($"[WebSocket] ConnectAsync 호출 - 연결 상태: {_isConnected}, 연결 중: {_isConnecting}"); | ||
Console.WriteLine($"[WebSocket] 토큰 상태 - AccessToken: {_tokenManager.GetAccessToken()?.Substring(0, 10) ?? "null"}, RefreshToken: {_tokenManager.GetRefreshToken()?.Substring(0, 10) ?? "null"}"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
URL 쿼리에 토큰 포함 및 로그 누출 방지
- JWT를 쿼리스트링으로 전달하면 프록시/서버 접근 로그에 노출됩니다. 가능하면 Authorization 헤더 또는 Sec-WebSocket-Protocol 토큰을 사용하세요. 부득이하면 단기 일회용 티켓으로 교체 권장.
- 현재 로그에 토큰이 그대로/부분 포함될 수 있습니다. 마스킹하세요.
- Console.WriteLine($"[WebSocket] ConnectAsync 호출 - 연결 상태: {_isConnected}, 연결 중: {_isConnecting}");
- Console.WriteLine($"[WebSocket] 토큰 상태 - AccessToken: {_tokenManager.GetAccessToken()?.Substring(0, 10) ?? "null"}, RefreshToken: {_tokenManager.GetRefreshToken()?.Substring(0, 10) ?? "null"}");
+ Debug.Log($"[WebSocket] ConnectAsync 호출 - 연결 상태: {_isConnected}, 연결 중: {_isConnecting}");
+ Debug.Log($"[WebSocket] 토큰 상태 확인 완료");
@@
- Debug.Log($"[WebSocket] 연결 시도 URL: {wsUrl.Substring(0, Math.Min(wsUrl.Length, 100))}...");
+ Debug.Log($"[WebSocket] 연결 시도 URL: {SanitizeWsUrlForLogs(wsUrl)}");
@@
- string accessToken = _tokenManager.GetAccessToken();
+ string accessToken = _tokenManager.GetAccessToken();
@@
- if (!string.IsNullOrEmpty(accessToken))
- {
- return $"{baseUrl}?token={accessToken}";
- }
+ if (!string.IsNullOrEmpty(accessToken))
+ {
+ var sep = baseUrl.Contains("?") ? "&" : "?";
+ var tokenParam = Uri.EscapeDataString(accessToken);
+ return $"{baseUrl}{sep}token={tokenParam}";
+ }
추가 헬퍼:
private static string SanitizeWsUrlForLogs(string url)
{
if (string.IsNullOrEmpty(url)) return url;
try
{
var u = new Uri(url);
var qs = System.Web.HttpUtility.ParseQueryString(u.Query);
if (qs["token"] != null) qs["token"] = "***";
var ub = new UriBuilder(u) { Query = qs.ToString() };
return ub.Uri.ToString();
}
catch { return url; }
}
Also applies to: 139-143, 259-270
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs around lines
107-109 (also apply same changes at 139-143 and 259-270), the current
implementation logs tokens and may pass JWTs in the WebSocket URL querystring;
update the connection code to send authentication via the Authorization header
or Sec-WebSocket-Protocol rather than embedding tokens in the URL, and stop
logging raw token values by masking them (e.g., replace with "***" or log
existence/first/last chars only). Additionally, sanitize any logged WebSocket
URLs by removing/masking token query parameters (use the provided
SanitizeWsUrlForLogs helper or equivalent) before logging, and if you must
include a token in the URL for legacy reasons, ensure it is a short-lived
one-time ticket and never printed in logs.
private void OnTokensUpdated(ProjectVG.Infrastructure.Auth.Models.TokenSet tokenSet) | ||
{ | ||
Debug.Log("[WebSocket] 토큰이 업데이트되었습니다. 연결을 시도합니다."); | ||
|
||
// 로그인 완료 시 WebSocket 자동 연결 | ||
if (!_isConnected && !_isConnecting) | ||
{ | ||
ConnectAsync().Forget(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// 토큰 클리어 이벤트 핸들러 - 로그아웃 시 연결 해제 | ||
/// </summary> | ||
private void OnTokensCleared() | ||
{ | ||
Debug.Log("[WebSocket] 토큰이 클리어되었습니다. 연결을 해제합니다."); | ||
|
||
// 로그아웃 시 WebSocket 연결 해제 | ||
_autoReconnect = false; | ||
DisconnectAsync().Forget(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
로그아웃 시 AutoReconnect 해제 후 재로그인 복원 처리
OnTokensCleared에서 _autoReconnect=false
로 비활성화하지만 재로그인 시 다시 true로 되돌리지 않아 재연결이 비활성화될 수 있습니다.
private void OnTokensUpdated(ProjectVG.Infrastructure.Auth.Models.TokenSet tokenSet)
{
Debug.Log("[WebSocket] 토큰이 업데이트되었습니다. 연결을 시도합니다.");
+ _autoReconnect = true; // 재로그인 후 자동 재연결 복원
if (!_isConnected && !_isConnecting)
{
ConnectAsync().Forget();
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private void OnTokensUpdated(ProjectVG.Infrastructure.Auth.Models.TokenSet tokenSet) | |
{ | |
Debug.Log("[WebSocket] 토큰이 업데이트되었습니다. 연결을 시도합니다."); | |
// 로그인 완료 시 WebSocket 자동 연결 | |
if (!_isConnected && !_isConnecting) | |
{ | |
ConnectAsync().Forget(); | |
} | |
} | |
/// <summary> | |
/// 토큰 클리어 이벤트 핸들러 - 로그아웃 시 연결 해제 | |
/// </summary> | |
private void OnTokensCleared() | |
{ | |
Debug.Log("[WebSocket] 토큰이 클리어되었습니다. 연결을 해제합니다."); | |
// 로그아웃 시 WebSocket 연결 해제 | |
_autoReconnect = false; | |
DisconnectAsync().Forget(); | |
} | |
private void OnTokensUpdated(ProjectVG.Infrastructure.Auth.Models.TokenSet tokenSet) | |
{ | |
Debug.Log("[WebSocket] 토큰이 업데이트되었습니다. 연결을 시도합니다."); | |
_autoReconnect = true; // 재로그인 후 자동 재연결 복원 | |
// 로그인 완료 시 WebSocket 자동 연결 | |
if (!_isConnected && !_isConnecting) | |
{ | |
ConnectAsync().Forget(); | |
} | |
} |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs around lines 484
to 505, OnTokensCleared sets _autoReconnect = false on logout but
OnTokensUpdated never restores it on login, so automatic reconnect may remain
disabled; update OnTokensUpdated to restore _autoReconnect (e.g., set
_autoReconnect = true before calling ConnectAsync()) or persist the previous
auto-reconnect state and reapply it on token update so that re-login re-enables
automatic reconnection.
{ | ||
"dependencies": { | ||
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", | ||
"com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Git 패키지 버전 고정 및 lock 커밋 필요
- 현재 com.endel.nativewebsocket은 브랜치/태그(#upm)만 지정되어 재현성이 낮습니다. 커밋 SHA로 고정하고 Packages/packages-lock.json을 함께 커밋해 주세요.
- 네트워크/런타임 영향이 커서 CI/에디터 버전에 따라 미묘한 차이가 발생할 수 있습니다.
권장 수정 예시(실제 커밋 SHA로 교체):
- "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm",
+ "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#<COMMIT_SHA>",
점검 스크립트:
추가 실수 방지(웹GL 메시지큐 호출 확인):
🏁 Script executed:
#!/bin/bash
# lock 파일 존재/고정 여부 확인
test -f Packages/packages-lock.json || { echo "packages-lock.json 미존재"; exit 1; }
if command -v jq >/dev/null 2>&1; then
jq '.dependencies["com.endel.nativewebsocket"]' Packages/packages-lock.json
else
rg -n '"com.endel.nativewebsocket"' Packages/packages-lock.json -n -C2
fi
Length of output: 308
🏁 Script executed:
#!/bin/bash
# WebGL에서 NativeWebSocket은 매 프레임 DispatchMessageQueue 호출 필요
rg -n 'DispatchMessageQueue\s*\(' -S -g 'Assets/**' || echo "DispatchMessageQueue 호출 코드가 보이지 않습니다(플랫폼 어댑터에서 처리했는지 확인)."
Length of output: 738
Packages/manifest.json: com.endel.nativewebsocket 버전을 커밋 SHA로 고정
Packages/packages-lock.json에서 해시 1d8b49b3fee41c09a98141f1f1a5e4db47e14229
로 잠겼으나, manifest.json은 여전히 #upm
태그를 가리킵니다. 재현성을 위해 아래처럼 커밋 SHA로 교체하세요.
- "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm",
+ "com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#1d8b49b3fee41c09a98141f1f1a5e4db47e14229",
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm", | |
"com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#1d8b49b3fee41c09a98141f1f1a5e4db47e14229", |
🤖 Prompt for AI Agents
In Packages/manifest.json around line 4, the package URL uses the unstable
"#upm" tag while Packages/packages-lock.json pins com.endel.nativewebsocket to
commit SHA 1d8b49b3fee41c09a98141f1f1a5e4db47e14229; update the manifest entry
to reference that exact commit SHA (replace "#upm" with
"#1d8b49b3fee41c09a98141f1f1a5e4db47e14229") so the dependency is reproducibly
fixed to the same revision recorded in the lock file.
"com.endel.nativewebsocket": { | ||
"version": "https://github.com/endel/NativeWebSocket.git#upm", | ||
"depth": 0, | ||
"source": "git", | ||
"dependencies": {}, | ||
"hash": "1d8b49b3fee41c09a98141f1f1a5e4db47e14229" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Git 패키지는 브랜치 대신 커밋 해시 고정 권장
com.endel.nativewebsocket
가 #upm
브랜치를 가리키고 있어 재현성/빌드 안정성이 흔들릴 수 있습니다. 특정 커밋 해시로 고정해 주세요.
com.endel.nativewebsocket 버전을 커밋 해시로 고정
Packages/packages-lock.json의 version
이 https://github.com/endel/NativeWebSocket.git#upm
처럼 브랜치를 가리키고 있어, 빌드 시마다 다른 커밋이 내려올 수 있습니다. 재현성과 안정성을 위해 특정 커밋 해시로 변경해 주세요.
🤖 Prompt for AI Agents
Packages/packages-lock.json around lines 10 to 16: 현재 version이 브랜치
태그(https://github.com/endel/NativeWebSocket.git#upm)로 되어 있어 빌드마다 다른 커밋이 내려올 수
있으니, 재현성을 위해 version 값을 특정 커밋 해시로 고정하세요; 이 파일의 동일 블록에 이미 있는 "hash"
값(1d8b49b3fee41c09a98141f1f1a5e4db47e14229)을 사용해 version을
https://github.com/endel/NativeWebSocket.git#1d8b49b3fee41c09a98141f1f1a5e4db47e14229
형태로 변경하고, 변경 후 lock 파일이 일관된 상태인지(다른 필드와 충돌 없는지) 확인하세요.
"key": "autoPublish", | ||
"value": "{\"m_Value\":true}" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
개인 PC 절대 경로와 autoPublish=true 커밋 — 팀/CI 환경 오염 가능
사용자별 경로(C:/Users/...
)는 타 환경에서 의미 없고 충돌을 야기합니다. autoPublish: true
는 의도치 않은 게시 위험도 있습니다. 기본 빌드 폴더 자동 생성 옵션을 유지하고 경로를 비워 두거나(패키지 기본값 사용), 팀 공용 상대 경로 정책을 별도 에디터 스크립트로 주입하는 방식을 권장합니다.
아래처럼 최소 수정:
{
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"key": "autoPublish",
- "value": "{\"m_Value\":true}"
+ "value": "{\"m_Value\":false}"
},
{
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"key": "buildOutputDirList",
- "value": "{\"m_Value\":\"C:/Users/imdls/Documents/Project/ProjectVG_Client/Build/Web;;;;;;;;;\"}"
+ "value": "{\"m_Value\":\"\"}"
}
추가로, 이 파일을 VCS 제외 대상으로 둘지 팀 정책을 결정해 주세요. 제외를 원하면 .gitignore에 다음을 추가:
+# Unity Connect Share(개발자별 설정)
+ProjectSettings/Packages/com.unity.connect.share/Settings.json
현재 값이 팀/CI에서 문제 유발 여부 점검:
Also applies to: 21-23
🏁 Script executed:
#!/bin/bash
# 절대 경로/윈도 경로 하드코딩 탐지(프로젝트 설정 전역)
rg -nP '"C:/' ProjectSettings || true
rg -n '"autoPublish","value":' ProjectSettings/Packages/com.unity.connect.share/Settings.json || true
Length of output: 314
🏁 Script executed:
#!/bin/bash
# autoPublish 설정 확인
rg -n '"key": *"autoPublish"' ProjectSettings/Packages/com.unity.connect.share/Settings.json || true
rg -n 'm_Value\":true' ProjectSettings/Packages/com.unity.connect.share/Settings.json || true
Length of output: 318
🏁 Script executed:
#!/bin/bash
# autoPublish 설정 확인을 위해 8~16번째 줄 출력
sed -n '8,16p' ProjectSettings/Packages/com.unity.connect.share/Settings.json
Length of output: 509
Settings.json에서 autoPublish 비활성화 및 절대경로 제거 필요
autoPublish=true는 의도치 않은 게시를 초래할 수 있으며, 사용자별 절대경로는 팀/CI 빌드에서 충돌을 일으킵니다.
diff --git a/ProjectSettings/Packages/com.unity.connect.share/Settings.json b/ProjectSettings/Packages/com.unity.connect.share/Settings.json
@@ -11,7 +11,7 @@
"key": "autoPublish",
- "value": "{\"m_Value\":true}"
+ "value": "{\"m_Value\":false}"
@@ -21,7 +21,7 @@
"key": "buildOutputDirList",
- "value": "{\"m_Value\":\"C:/Users/imdls/Documents/Project/ProjectVG_Client/Build/Web;;;;;;;;;\"}"
+ "value": "{\"m_Value\":\"\"}"
필요 시 해당 파일을 VCS에서 제외(ProjectSettings/Packages/com.unity.connect.share/Settings.json 추가)하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"key": "autoPublish", | |
"value": "{\"m_Value\":true}" | |
}, | |
{ | |
"key": "autoPublish", | |
"value": "{\"m_Value\":false}" | |
}, | |
{ | |
"key": "buildOutputDirList", | |
"value": "{\"m_Value\":\"\"}" | |
} |
🤖 Prompt for AI Agents
ProjectSettings/Packages/com.unity.connect.share/Settings.json lines 11-13:
disable autoPublish and remove any user-specific absolute paths; change the
"autoPublish" entry value from true to false (set the JSON value to represent
false) and scan the file for any absolute file-system paths and replace them
with relative paths or remove them, then add
ProjectSettings/Packages/com.unity.connect.share/Settings.json to VCS ignore (or
exclude it from source control) so personal settings don't get committed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | ||
{ | ||
yield return new WaitForSeconds(0.1f); // Priority 충돌 방지용 짧은 대기 | ||
|
||
if (_motionController != null) | ||
{ | ||
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | ||
_motionController.PlayAnimation(clip, priority: 2, isLoop: false); | ||
_isPlayingAction = true; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
priority 매개변수가 무시되고 하드코딩(2)되어 있습니다
Idle/Normal 우선순위 충돌 방지 의도가 무력화됩니다.
- _motionController.PlayAnimation(clip, priority: 2, isLoop: false);
+ _motionController.PlayAnimation(clip, priority: priority, isLoop: false);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | |
{ | |
yield return new WaitForSeconds(0.1f); // Priority 충돌 방지용 짧은 대기 | |
if (_motionController != null) | |
{ | |
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | |
_motionController.PlayAnimation(clip, priority: 2, isLoop: false); | |
_isPlayingAction = true; | |
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | |
{ | |
yield return new WaitForSeconds(0.1f); // Priority 충돌 방지용 짧은 대기 | |
if (_motionController != null) | |
{ | |
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | |
_motionController.PlayAnimation(clip, priority: priority, isLoop: false); | |
_isPlayingAction = true; | |
… |
🤖 Prompt for AI Agents
In Assets/Domain/Character/Script/CharacterActionController.cs around lines 312
to 321, the DelayedMotionPlay method ignores the incoming priority parameter and
uses a hardcoded value (2), defeating the intended idle/normal priority conflict
avoidance; change the call to _motionController.PlayAnimation to pass the
method's priority parameter (e.g., priority: priority) instead of 2, leaving
isLoop:false and the surrounding null/check and _isPlayingAction assignment
intact so the original behavior and fade handling remain unchanged.
// HTTP 헤더에서 추출할 토큰 정보 | ||
[JsonIgnore] | ||
public string AccessToken { get; set; } | ||
|
||
[JsonIgnore] | ||
public string RefreshToken { get; set; } | ||
|
||
[JsonIgnore] | ||
public int ExpiresIn { get; set; } | ||
|
||
[JsonIgnore] | ||
public string UserId { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ServerOAuth2TokenResponse에서 토큰 필드가 역직렬화되지 않아 갱신 실패 가능성이 큼
[JsonIgnore]
로 인해 PostAsync<ServerOAuth2TokenResponse>
호출 시 본문에 토큰이 와도 모두 null입니다. 현재 TokenRefreshService는 헤더를 읽지 않고 본문 모델을 사용하므로 NRE/갱신 실패가 발생합니다. 최소 한 경로(본문 또는 헤더)로 값을 받을 수 있게 해야 합니다. 빠른 복구를 위해 본문 매핑을 허용하는 쪽을 제안합니다.
다음 패치를 적용하면 본문 응답으로도 동작합니다(헤더 기반 파싱을 병행하려면 HttpApiClient에 PostWithHeaders 추가를 별도 코멘트로 제안합니다):
- // HTTP 헤더에서 추출할 토큰 정보
- [JsonIgnore]
- public string AccessToken { get; set; }
-
- [JsonIgnore]
- public string RefreshToken { get; set; }
-
- [JsonIgnore]
- public int ExpiresIn { get; set; }
-
- [JsonIgnore]
- public string UserId { get; set; }
+ // 본문 응답에서도 수신 가능하도록 매핑 (서버가 헤더를 사용할 경우 병행 가능)
+ [JsonProperty("accessToken")]
+ public string AccessToken { get; set; }
+
+ [JsonProperty("refreshToken")]
+ public string RefreshToken { get; set; }
+
+ [JsonProperty("expiresIn")]
+ public int ExpiresIn { get; set; }
+
+ [JsonProperty("userId")]
+ public string UserId { get; set; }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// HTTP 헤더에서 추출할 토큰 정보 | |
[JsonIgnore] | |
public string AccessToken { get; set; } | |
[JsonIgnore] | |
public string RefreshToken { get; set; } | |
[JsonIgnore] | |
public int ExpiresIn { get; set; } | |
[JsonIgnore] | |
public string UserId { get; set; } | |
// 본문 응답에서도 수신 가능하도록 매핑 (서버가 헤더를 사용할 경우 병행 가능) | |
[JsonProperty("accessToken")] | |
public string AccessToken { get; set; } | |
[JsonProperty("refreshToken")] | |
public string RefreshToken { get; set; } | |
[JsonProperty("expiresIn")] | |
public int ExpiresIn { get; set; } | |
[JsonProperty("userId")] | |
public string UserId { get; set; } |
private string EncryptData(string data) | ||
{ | ||
try | ||
{ | ||
using (var aes = Aes.Create()) | ||
{ | ||
aes.Key = Encoding.UTF8.GetBytes(ENCRYPTION_KEY.PadRight(32, '0').Substring(0, 32)); | ||
aes.IV = new byte[16]; | ||
|
||
using (var encryptor = aes.CreateEncryptor()) | ||
using (var ms = new System.IO.MemoryStream()) | ||
{ | ||
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) | ||
using (var sw = new System.IO.StreamWriter(cs)) | ||
{ | ||
sw.Write(data); | ||
} | ||
return Convert.ToBase64String(ms.ToArray()); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
AES 보안 취약점: 고정 IV(0)와 하드코딩 키 사용
현재 AES IV를 new byte[16]
으로 고정하고, 고정 키 문자열을 코드에 보관합니다. 이는 재암호화 시 동일한 IV로 인해 평문 패턴이 노출되고, 키 유출 위험이 큽니다. 최소한 매 암호화마다 랜덤 IV를 생성해 암호문에 프리픽스로 저장/복원하세요. 가능하면 인증암호(AEAD: AES-GCM) 또는 HMAC 무결성 검증도 추가하세요.
private string EncryptData(string data)
{
try
{
using (var aes = Aes.Create())
{
- aes.Key = Encoding.UTF8.GetBytes(ENCRYPTION_KEY.PadRight(32, '0').Substring(0, 32));
- aes.IV = new byte[16];
+ aes.Key = Encoding.UTF8.GetBytes(ENCRYPTION_KEY.PadRight(32, '0').Substring(0, 32));
+ aes.GenerateIV(); // 랜덤 IV
- using (var encryptor = aes.CreateEncryptor())
- using (var ms = new System.IO.MemoryStream())
+ using (var encryptor = aes.CreateEncryptor())
+ using (var ms = new System.IO.MemoryStream())
{
- using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
- using (var sw = new System.IO.StreamWriter(cs))
- {
- sw.Write(data);
- }
- return Convert.ToBase64String(ms.ToArray());
+ // 먼저 IV를 기록
+ ms.Write(aes.IV, 0, aes.IV.Length);
+ using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
+ using (var sw = new System.IO.StreamWriter(cs))
+ {
+ sw.Write(data);
+ }
+ return Convert.ToBase64String(ms.ToArray());
}
}
}
catch (Exception ex)
{
Debug.LogError($"[TokenManager] 암호화 실패: {ex.Message}");
throw;
}
}
private string DecryptData(string encryptedData)
{
try
{
using (var aes = Aes.Create())
{
- aes.Key = Encoding.UTF8.GetBytes(ENCRYPTION_KEY.PadRight(32, '0').Substring(0, 32));
- aes.IV = new byte[16];
+ aes.Key = Encoding.UTF8.GetBytes(ENCRYPTION_KEY.PadRight(32, '0').Substring(0, 32));
- using (var decryptor = aes.CreateDecryptor())
- using (var ms = new System.IO.MemoryStream(Convert.FromBase64String(encryptedData)))
- using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
- using (var sr = new System.IO.StreamReader(cs))
- {
- return sr.ReadToEnd();
- }
+ var allBytes = Convert.FromBase64String(encryptedData);
+ if (allBytes.Length < 16) throw new InvalidOperationException("암호문 길이가 유효하지 않습니다.");
+ var iv = new byte[16];
+ Buffer.BlockCopy(allBytes, 0, iv, 0, iv.Length);
+ var cipher = new byte[allBytes.Length - iv.Length];
+ Buffer.BlockCopy(allBytes, iv.Length, cipher, 0, cipher.Length);
+ aes.IV = iv;
+
+ using (var decryptor = aes.CreateDecryptor())
+ using (var ms = new System.IO.MemoryStream(cipher))
+ using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
+ using (var sr = new System.IO.StreamReader(cs))
+ {
+ return sr.ReadToEnd();
+ }
}
}
catch (Exception ex)
{
Debug.LogError($"[TokenManager] 복호화 실패: {ex.Message}");
throw;
}
}
추가로, PlayerPrefs는 쉽게 추출됩니다. 플랫폼 보안 저장소(iOS Keychain/Android Keystore/Windows DPAPI 등) 사용을 검토하세요. 키 관리도 빌드별 시딩/분할 저장/OS 비밀 저장소 연동 등으로 강화가 필요합니다.
Also applies to: 259-282
public TokenSet ToTokenSet() | ||
{ | ||
if (Tokens == null) | ||
{ | ||
return null; | ||
} | ||
|
||
var accessToken = new AccessToken(Tokens.AccessToken); | ||
|
||
RefreshToken refreshToken = null; | ||
if (!string.IsNullOrEmpty(Tokens.RefreshToken)) | ||
{ | ||
refreshToken = new RefreshToken( | ||
Tokens.RefreshToken, | ||
Tokens.RefreshExpiresIn, | ||
User?.UserId ?? "guest" | ||
); | ||
} | ||
|
||
return new TokenSet(accessToken, refreshToken); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AccessToken null/빈 값 방어 로직 누락 시 런타임 예외 발생
new AccessToken(Tokens.AccessToken)
에서 null이면 예외가 발생합니다. 방어 로직을 추가하세요.
public TokenSet ToTokenSet()
{
if (Tokens == null)
{
return null;
}
- var accessToken = new AccessToken(Tokens.AccessToken);
+ if (string.IsNullOrEmpty(Tokens.AccessToken))
+ {
+ // 서버 응답에 액세스 토큰이 없으면 TokenSet 생성 불가
+ return null;
+ }
+ var accessToken = new AccessToken(Tokens.AccessToken);
추가로, 서버가 JWT가 아닌 토큰을 줄 수 있다면 ExpiresIn
기반 만료 설정 경로도 별도 고려가 필요합니다(현 구조에선 JWT exp 파싱 실패 시 ExpiresAt = DateTime.MinValue
가 됩니다).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public TokenSet ToTokenSet() | |
{ | |
if (Tokens == null) | |
{ | |
return null; | |
} | |
var accessToken = new AccessToken(Tokens.AccessToken); | |
RefreshToken refreshToken = null; | |
if (!string.IsNullOrEmpty(Tokens.RefreshToken)) | |
{ | |
refreshToken = new RefreshToken( | |
Tokens.RefreshToken, | |
Tokens.RefreshExpiresIn, | |
User?.UserId ?? "guest" | |
); | |
} | |
return new TokenSet(accessToken, refreshToken); | |
} | |
public TokenSet ToTokenSet() | |
{ | |
if (Tokens == null) | |
{ | |
return null; | |
} | |
if (string.IsNullOrEmpty(Tokens.AccessToken)) | |
{ | |
// 서버 응답에 액세스 토큰이 없으면 TokenSet 생성 불가 | |
return null; | |
} | |
var accessToken = new AccessToken(Tokens.AccessToken); | |
RefreshToken refreshToken = null; | |
if (!string.IsNullOrEmpty(Tokens.RefreshToken)) | |
{ | |
refreshToken = new RefreshToken( | |
Tokens.RefreshToken, | |
Tokens.RefreshExpiresIn, | |
User?.UserId ?? "guest" | |
); | |
} | |
return new TokenSet(accessToken, refreshToken); | |
} |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/DTOs/Auth/GuestLoginResponse.cs around lines
40-60, the code blindly constructs an AccessToken from Tokens.AccessToken which
will throw if that value is null/empty and also doesn't handle non-JWT tokens'
expiry; update the method to first check if Tokens.AccessToken is null or empty
and avoid constructing an AccessToken (return null or a TokenSet with only a
refresh token per your domain rules), and when creating the AccessToken attempt
JWT parsing but fall back to setting ExpiresAt using Tokens.ExpiresIn (e.g.,
DateTime.UtcNow + TimeSpan.FromSeconds(Tokens.ExpiresIn)) when parsing fails or
the token is not a JWT; keep the existing refresh token creation logic and
retain User?.UserId ?? "guest" for the refresh token.
Debug.Log($"[HttpApiClient] Access Token: {accessToken}"); | ||
|
||
if (!string.IsNullOrEmpty(accessToken)) | ||
{ | ||
SetAuthToken(accessToken); | ||
} | ||
else | ||
{ | ||
Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다."); | ||
RemoveDefaultHeader(AUTHORIZATION_HEADER); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
민감정보 노출: Access Token 전체가 로그로 출력됨
토큰 전체를 로그로 남기면 보안 사고로 직결됩니다. 즉시 제거하거나 마스킹하세요.
- Debug.Log($"[HttpApiClient] Access Token: {accessToken}");
+ // 민감정보(토큰)는 로그에 출력하지 않습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
Debug.Log($"[HttpApiClient] Access Token: {accessToken}"); | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
SetAuthToken(accessToken); | |
} | |
else | |
{ | |
Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다."); | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
} | |
} | |
// 민감정보(토큰)는 로그에 출력하지 않습니다. | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
SetAuthToken(accessToken); | |
} | |
else | |
{ | |
Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다."); | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
} |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/Http/HttpApiClient.cs around lines 81 to 92,
the current Debug.Log prints the full access token which exposes sensitive
credentials; remove that Debug.Log call or replace it with a non-sensitive
alternative (e.g., log only a masked token like "****" + last 4 chars or a token
hash/ID), ensure any conditional branches do not log the full token elsewhere,
and run a quick grep to confirm no other code prints full tokens to logs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
♻️ Duplicate comments (11)
Assets/Domain/Character/Script/CharacterActionController.cs (1)
309-327
: priority 매개변수 무시 + 코루틴 정리 순서/범위 오류로 콜백 유실/자기중단 위험
DelayedMotionPlay
내부에서priority
인자를 무시하고 하드코딩2
를 사용하고 있습니다. Idle/Normal 우선순위 분리가 깨집니다._currentMotionEndCallback
를 설정한 직후StopAllMotionCoroutines()
로 곧바로 null 초기화되어 콜백이 유실됩니다.- 더 심각하게, 코루틴 내부에서
StopAllCoroutines()
를 호출하면 자신(현재 코루틴)까지 중단될 수 있어 이후 로직(새WaitForMotionEnd
시작)이 실행되지 않을 수 있습니다.아래와 같이 “기존 종료 대기 코루틴만 안전하게 중지”하고,
priority
를 전달하며, 콜백 설정/시작 순서를 바로잡아 주세요.@@ - if (_motionController != null) - { - // 모션 재생 (CubismFadeController가 자동 페이드 처리) - _motionController.PlayAnimation(clip, priority: 2, isLoop: false); - _isPlayingAction = true; - - // 모션 종료 처리 설정 - _currentMotionEndCallback = endCallback; - StopAllMotionCoroutines(); - _currentMotionCoroutine = StartCoroutine(WaitForMotionEnd(clip.length, endCallback)); - - Debug.Log($"[CharacterActionController] 모션 재생: {_currentMotionClip?.Id} (그룹: {_currentMotionClip?.MotionGroup}), 길이: {clip.length}s"); - } + if (_motionController != null) + { + // 기존 종료 대기 코루틴만 안전하게 정리 (자기 자신 포함 모든 코루틴 중지는 금지) + if (_currentMotionCoroutine != null) + { + StopCoroutine(_currentMotionCoroutine); + _currentMotionCoroutine = null; + } + _currentMotionEndCallback = null; + + // 모션 재생 (CubismFadeController가 자동 페이드 처리) + _motionController.PlayAnimation(clip, priority: priority, isLoop: false); + _isPlayingAction = true; + + // 모션 종료 처리 설정 + _currentMotionEndCallback = endCallback; + _currentMotionCoroutine = StartCoroutine(WaitForMotionEnd(clip.length, endCallback)); + + Debug.Log($"[CharacterActionController] 모션 재생: {_currentMotionClip?.Id} (그룹: {_currentMotionClip?.MotionGroup}), 길이: {clip.length}s"); + }Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs (4)
1-5
: 레포 전반 System.Web 사용처 잔존 여부 확인 요청빌드 파이프라인 이슈를 사전에 차단하기 위해 검증 스크립트 실행을 권장합니다.
#!/bin/bash # 레포 내 System.Web 및 HttpUtility 사용처 점검 rg -nP --type=cs '\bSystem\.Web\b|HttpUtility'
1-5
: System.Web 제거 및 크로스플랫폼 QueryString 파서로 교체 필요 (IL2CPP/WebGL 호환성 이슈)Unity(WebGL/IL2CPP/.NET Standard)에서
System.Web
미지원으로 빌드 실패 위험이 큽니다.HttpUtility.ParseQueryString
의존성을 제거하고System.Net.WebUtility
+ 내부 파서로 통일하세요.아래 변경을 제안합니다.
-using System.Web; +using System.Net;- var uri = new Uri(callbackUrl); - var query = HttpUtility.ParseQueryString(uri.Query); - - // 쿼리 파라미터를 Dictionary로 변환 (대소문자 무시) - var queryParams = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - foreach (string key in query.AllKeys) - { - if (!string.IsNullOrEmpty(key)) - { - queryParams[key] = query[key]; - } - } + var uri = new Uri(callbackUrl); + var queryParams = ParseQueryToDictionary(uri.Query);- var queryString = schemeUrl.Substring(queryStartIndex + 1); - var query = HttpUtility.ParseQueryString(queryString); - - // 쿼리 파라미터를 Dictionary로 변환 - var queryParams = new Dictionary<string, string>(); - foreach (string key in query.AllKeys) - { - if (!string.IsNullOrEmpty(key)) - { - queryParams[key] = query[key]; - } - } + var queryString = schemeUrl.Substring(queryStartIndex + 1); + var queryParams = ParseQueryToDictionary(queryString);- var uri = new Uri(url); - var query = HttpUtility.ParseQueryString(uri.Query); - return query[parameterName]; + var uri = new Uri(url); + var dict = ParseQueryToDictionary(uri.Query); + return dict.TryGetValue(parameterName, out var v) ? v : null;- var uri = new Uri(url); - var query = HttpUtility.ParseQueryString(uri.Query); - - // success 파라미터가 있으면 OAuth2 콜백으로 간주 - return query.AllKeys.Contains("success"); + var uri = new Uri(url); + var dict = ParseQueryToDictionary(uri.Query); + return dict.ContainsKey("success");추가: 내부 QueryString 파서(클래스 내부 private 메서드로 삽입)
private static Dictionary<string, string> ParseQueryToDictionary(string queryOrUrlPart) { var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(queryOrUrlPart)) return result; if (queryOrUrlPart[0] == '?' || queryOrUrlPart[0] == '#') queryOrUrlPart = queryOrUrlPart.Substring(1); var pairs = queryOrUrlPart.Split('&'); foreach (var pair in pairs) { if (string.IsNullOrEmpty(pair)) continue; var idx = pair.IndexOf('='); string key, value; if (idx >= 0) { key = pair.Substring(0, idx); value = pair.Substring(idx + 1); } else { key = pair; value = null; } if (string.IsNullOrEmpty(key)) continue; key = WebUtility.UrlDecode(key); value = value != null ? WebUtility.UrlDecode(value) : null; result[key] = value; // 중복 키는 마지막 값 우선 } return result; }Also applies to: 28-41, 108-119, 176-181, 200-207
121-130
: ParseSchemeUrl: success 값 비어있을 때 NRE 및 대소문자 민감 문제
queryParams["success"].ToLower()
는 null에서 NRE를 유발합니다. 또한 현재 딕셔너리는 대소문자 민감합니다.- // success 파라미터 확인 - if (!queryParams.ContainsKey("success")) - { - return OAuth2CallbackResult.ErrorResult("success 파라미터가 없습니다.", schemeUrl); - } - - var success = queryParams["success"].ToLower(); - - if (success == "true") + // success 파라미터 확인 + if (!queryParams.TryGetValue("success", out var successRaw) || string.IsNullOrEmpty(successRaw)) + { + return OAuth2CallbackResult.ErrorResult("success 파라미터가 비어있습니다.", schemeUrl); + } + var success = bool.TryParse(successRaw, out var s) && s; + + if (success) {
171-186
: ExtractParameter: 파서 일원화 및 대소문자 무시 조회내부 파서를 사용해 케이스 인식/디코딩을 통일하세요.
HttpUtility
제거와 함께 동작 일관성을 확보합니다.- var uri = new Uri(url); - var query = HttpUtility.ParseQueryString(uri.Query); - return query[parameterName]; + var uri = new Uri(url); + var dict = ParseQueryToDictionary(uri.Query); + return dict.TryGetValue(parameterName, out var v) ? v : null;Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs (1)
16-24
: 콜백 URL/대기 플래그의 스레드-가시성 보장 필요HttpListener 스레드에서 쓰고 메인 루프에서 읽습니다. 가시성 미보장으로 콜백을 놓칠 수 있습니다. volatile 지정(또는 Volatile/Interlocked 사용)이 필요합니다. 이전 코멘트와 동일 맥락입니다.
- private string _callbackUrl; + private volatile string _callbackUrl; @@ - private bool _isWaitingForCallback = false; + private volatile bool _isWaitingForCallback = false;Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
32-38
: null 요청 가드 추가 필요
request
가 null이면 내부 HTTP 호출에서 즉시 NRE가 발생합니다. 명시적인 null 체크를 추가하여 더 명확한 에러 메시지를 제공해야 합니다.public async UniTask<ChatResponse> SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) { ValidateHttpClient(); + if (request == null) + throw new ArgumentNullException(nameof(request)); - return await _httpClient.PostAsync<ChatResponse>($"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); + return await _httpClient.PostAsync<ChatResponse>( + $"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); }Assets/Infrastructure/Auth/Services/GuestAuthService.cs (2)
82-86
: 서버 요청 페이로드 불일치 가능성
GuestLoginRequest
객체를 생성했지만 실제로는deviceId
문자열만 전송하고 있습니다. 서버가 JSON 본문{ "guestId": "..." }
를 기대한다면 실패할 수 있습니다.var response = await _httpClient.PostAsync<GuestLoginResponse>( "api/v1/auth/guest-login", - deviceId, // 서버는 [FromBody] string guestId를 받음 + request, // DTO(JSON) { guestId } 전달 requiresAuth: false );
110-114
: 액세스 토큰 평문 로깅 제거 (보안 이슈)토큰 전체를 로그로 남기면 유출 시 계정 탈취로 직결됩니다. 마스킹하고 개발 빌드에서만 출력하세요.
Debug.Log("[GuestAuthService] Guest 로그인 성공"); +#if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log($"[GuestAuthService] 사용자 ID: {response.User?.UserId}"); -Debug.Log($"[GuestAuthService] AccessToken : {tokenSet.AccessToken.Token}"); +Debug.Log($"[GuestAuthService] AccessToken(마스킹): {MaskDeviceId(tokenSet.AccessToken.Token)}"); Debug.Log($"[GuestAuthService] AccessToken 만료: {tokenSet.AccessToken.ExpiresAt}"); +#endifAssets/Infrastructure/Auth/TokenRefreshService.cs (1)
173-180
: 서버가 Refresh Token을 회전하지 않을 때 기존 RT 보존 필요응답에 RefreshToken이 없으면 현재 구현은 저장 시 RT를 삭제합니다. 이는 재로그인을 강요하게 됩니다. 기존 RT를 유지해야 합니다.
var accessToken = new AccessToken(response.AccessToken); -var newRefreshToken = !string.IsNullOrEmpty(response.RefreshToken) - ? new RefreshToken(response.RefreshToken, response.ExpiresIn * 2, response.UserId) // UserId를 DeviceId로 사용 - : null; +// 서버가 새 refresh token을 주지 않으면 기존 것 유지 +var currentRefreshToken = _tokenManager.GetRefreshToken(); +RefreshToken newRefreshToken = null; + +if (!string.IsNullOrEmpty(response.RefreshToken)) +{ + newRefreshToken = new RefreshToken(response.RefreshToken, response.ExpiresIn * 2, response.UserId); +} +else if (!string.IsNullOrEmpty(currentRefreshToken)) +{ + // 기존 RefreshToken 객체를 재생성하거나 유지 + // TokenManager에서 RefreshToken 객체를 가져올 수 있는 메서드가 필요할 수 있음 + newRefreshToken = _tokenManager.LoadTokens()?.RefreshToken; +} return new TokenSet(accessToken, newRefreshToken);Assets/Infrastructure/Network/Http/HttpApiClient.cs (1)
81-81
: 민감정보 노출: Access Token 전체가 로그로 출력됨토큰 전체를 로그로 남기면 보안 사고로 직결됩니다. 즉시 제거하거나 마스킹하세요.
-Debug.Log($"[HttpApiClient] Access Token: {accessToken}"); +// 민감정보(토큰)는 로그에 출력하지 않습니다. +#if UNITY_EDITOR && DEVELOPMENT_BUILD +Debug.Log($"[HttpApiClient] Access Token 길이: {accessToken?.Length ?? 0}"); +#endif
🧹 Nitpick comments (29)
Assets/Core/DebugConsole/GameDebugConsoleManager.cs (4)
59-59
: Start에서의 진단 호출은 릴리스 빌드에서 제거되도록 처리하세요ValidateButtonSetup()을 Start에서 항상 호출하면 실서비스 로그 노이즈가 생깁니다. 메서드에 Conditional을 붙이면 호출부는 그대로 두고도 에디터/개발 빌드에서만 실행됩니다. (이전 학습된 선호에 따라 “초기 로그 확인”이 필요하면 _settings에 StartVisibleInDev 같은 플래그를 두고 Debug.isDebugBuild와 함께 제어하는 것도 권장합니다.)
아래처럼 메서드 선언에 속성을 추가하세요:
- public void ValidateButtonSetup() + [System.Diagnostics.Conditional("UNITY_EDITOR")] + [System.Diagnostics.Conditional("DEVELOPMENT_BUILD")] + public void ValidateButtonSetup()
119-149
: SetupUI의 Debug.Log는 디버그 빌드에서만 출력하도록 가드하세요[DEBUG_CONSOLE] 프리픽스 사용으로 재귀 로깅은 잘 피하고 있습니다만, 릴리스 빌드에서 불필요한 문자열 보간/할당 비용을 피하기 위해 가드를 권장합니다. Warning은 유지하고, Log만 가드해도 충분합니다.
- Debug.Log("[DEBUG_CONSOLE] SetupUI called"); + if (Debug.isDebugBuild) Debug.Log("[DEBUG_CONSOLE] SetupUI called"); - Debug.Log("[DEBUG_CONSOLE] Clear button listener added"); + if (Debug.isDebugBuild) Debug.Log("[DEBUG_CONSOLE] Clear button listener added"); - Debug.Log("[DEBUG_CONSOLE] Toggle button listener added"); + if (Debug.isDebugBuild) Debug.Log("[DEBUG_CONSOLE] Toggle button listener added"); - Debug.Log("[DEBUG_CONSOLE] Filter input listener added"); + if (Debug.isDebugBuild) Debug.Log("[DEBUG_CONSOLE] Filter input listener added");
362-374
: 토글 동작 로그도 디버그 빌드 가드를 적용하세요실서비스에서의 로그 스팸/문자열 보간 비용을 줄일 수 있습니다. Warning은 그대로 두세요.
- Debug.Log($"[DEBUG_CONSOLE] ToggleConsole called. Current state: {_isConsoleVisible}"); + if (Debug.isDebugBuild) Debug.Log($"[DEBUG_CONSOLE] ToggleConsole called. Current state: {_isConsoleVisible}"); ... - Debug.Log($"[DEBUG_CONSOLE] Console panel set to: {_isConsoleVisible}"); + if (Debug.isDebugBuild) Debug.Log($"[DEBUG_CONSOLE] Console panel set to: {_isConsoleVisible}");
545-573
: 검증 로그 문구 정확화(‘persistent’ 명시) 및 호출 맥락 주의GetPersistentEventCount()는 에디터에 저장된 Persistent 호출만 집계합니다. SetupUI에서 AddListener로 추가한 런타임 리스너는 포함되지 않아 혼동을 줄 수 있습니다. 문구를 명확히 해 주세요. (메서드 자체는 위 코멘트처럼 Conditional로 제한 권장)
- Debug.Log($"[DEBUG_CONSOLE] Toggle button onClick event count: {_toggleButton.onClick.GetPersistentEventCount()}"); + Debug.Log($"[DEBUG_CONSOLE] Toggle button onClick persistent event count: {_toggleButton.onClick.GetPersistentEventCount()}");Assets/Domain/Character/Script/CharacterActionController.cs (4)
385-393
: 주석과 구현 불일치: 0.3초 딜레이 반영 필요주석은 “0.3초 딜레이”인데 실제 값이
0.0f
입니다. Idle 루프가 과도하게 촘촘해질 수 있습니다.- yield return new WaitForSeconds(0.0f); + yield return new WaitForSeconds(0.3f);
120-124
: 비활성화 시 애니메이션까지 정지해 누수/잔상 방지 권장
OnDisable
에서 코루틴만 중지합니다. Live2D 재생 자체도 멈추도록ForceStopMotion()
호출을 추가하는 것이 안전합니다.private void OnDisable() { StopAllMotionCoroutines(); + ForceStopMotion(); }
51-86
: 재초기화 안전성: 이벤트 중복 구독 방지 및 과도 로그 가드 권장
Initialize
가 여러 번 호출될 경우를 대비해 기존 컨트롤러에서 이벤트 핸들러를 먼저 해제 후 새 컨트롤러에 구독하세요._motionClips
전체를 순회하며 출력하는 디버그 로그는 런타임 스팸 위험이 있습니다. 에디터 전용 가드(#if UNITY_EDITOR
)나 플래그로 제어를 권장합니다.- _motionController = motionController; + // 기존 컨트롤러 핸들러 해제 + if (_motionController != null) + { + _motionController.AnimationEndHandler -= OnLive2DMotionEnd; + } + _motionController = motionController; @@ - if (_motionClips != null) + #if UNITY_EDITOR + if (_motionClips != null) { foreach (var clip in _motionClips) { Debug.Log($"[CharacterActionController] Motion Clip: {clip.Id} | Group: '{clip.MotionGroup}' | AnimationClip: {(clip.animationClip != null ? "O" : "X")}"); } @@ var idleClips = _motionClips.Where(c => c.MotionGroup.Equals("idle", System.StringComparison.OrdinalIgnoreCase)).ToList(); Debug.Log($"[CharacterActionController] idle 그룹 모션 클립 수: {idleClips.Count}개"); } + #endif
479-485
: StopAllCoroutines 사용 주의 사항 명시 또는 래핑 함수 분리 제안이 메서드는
StopAllCoroutines()
를 호출하므로 코루틴 내부에서 호출 시 자기 자신까지 중지됩니다. 호출 컨텍스트 제약을 주석으로 명시하거나 “종료 대기 코루틴만 중지”하는 전용 헬퍼를 분리해 오용을 방지하세요.Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs (5)
47-51
: 불리언 파싱 일관성 개선문자열 비교 대신
bool.TryParse
사용을 권장합니다. 추후 "1"/"0" 등 확장 필요 시 손쉽게 보완 가능합니다.- var success = successRaw.Equals("true", StringComparison.OrdinalIgnoreCase); + var success = bool.TryParse(successRaw, out var s) && s;
69-75
: OAuth 오류 메시지 가독성 향상: error_description 우선 사용OAuth2 표준/관행상
error_description
이 상세 메시지를 담습니다. 존재 시 우선 사용을 권장합니다.- var error = queryParams.ContainsKey("error") ? queryParams["error"] : "알 수 없는 오류"; + var error = queryParams.TryGetValue("error_description", out var desc) && !string.IsNullOrEmpty(desc) + ? desc + : (queryParams.TryGetValue("error", out var err) && !string.IsNullOrEmpty(err) ? err : "알 수 없는 오류");
101-109
: 커스텀 스킴에서 프래그먼트(#)로 전달되는 케이스 보완(옵션)일부 구현은
#
뒤로 전달합니다. 쿼리 미존재 시 프래그먼트도 수용하는 방어 코드를 권장합니다.- var queryStartIndex = schemeUrl.IndexOf('?'); - if (queryStartIndex == -1) + var qIndex = schemeUrl.IndexOf('?'); + var hIndex = schemeUrl.IndexOf('#'); + if (qIndex == -1 && hIndex == -1) { return OAuth2CallbackResult.ErrorResult("쿼리 파라미터가 없습니다.", schemeUrl); } - - var queryString = schemeUrl.Substring(queryStartIndex + 1); + var start = qIndex != -1 ? qIndex + 1 : hIndex + 1; + var queryString = schemeUrl.Substring(start);
220-250
: IsCustomScheme: Uri 생성 시 예외 대신 TryCreate 사용입력 불량으로 인한 예외 비용을 줄이고 false로 안전 탈출하세요.
- try - { - var uri = new Uri(url); + try + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + return false; + }
257-281
: 디버그 문자열 빌드 성능·안정성 개선(StringBuilder 사용) 및 로그 노출 주의
StringBuilder
사용을 권장합니다. 또한 운영 로그에는 민감 값(토큰/코드 등) 포함 가능성에 주의하십시오(필요 시 마스킹/레벨 제한).- var info = $"OAuth2 Callback Debug Info:\n"; - info += $"Success: {callbackResult.Success}\n"; - info += $"State: {callbackResult.State}\n"; - info += $"Error: {callbackResult.Error}\n"; - info += $"Original URL: {callbackResult.OriginalUrl}\n"; - info += $"Query Parameters Count: {callbackResult.QueryParameters?.Count ?? 0}\n"; - - if (callbackResult.QueryParameters != null && callbackResult.QueryParameters.Count > 0) - { - info += "Query Parameters:\n"; - foreach (var kvp in callbackResult.QueryParameters) - { - info += $" {kvp.Key}: {kvp.Value}\n"; - } - } - - return info; + var sb = new System.Text.StringBuilder(); + sb.AppendLine("OAuth2 Callback Debug Info:"); + sb.AppendLine($"Success: {callbackResult.Success}"); + sb.AppendLine($"State: {callbackResult.State}"); + sb.AppendLine($"Error: {callbackResult.Error}"); + sb.AppendLine($"Original URL: {callbackResult.OriginalUrl}"); + sb.AppendLine($"Query Parameters Count: {callbackResult.QueryParameters?.Count ?? 0}"); + if (callbackResult.QueryParameters is { Count: > 0 }) + { + sb.AppendLine("Query Parameters:"); + foreach (var kvp in callbackResult.QueryParameters) + { + sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + } + } + return sb.ToString();Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs (8)
85-97
: CancellationTokenSource 해제 누락Cancel만 호출되고 Dispose가 누락되어 있습니다.
public void Cleanup() { _isDisposed = true; _isWaitingForCallback = false; _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); _listener?.Stop(); _listener?.Close();
108-114
: 리다이렉트 호스트 검증(루프백 강제/경고)보안상 데스크톱 로컬 서버는 루프백 전용이어야 합니다. 설정이 실수로 0.0.0.0/사설 IP로 지정되면 외부 접근 노출 가능성이 있습니다. uri.IsLoopback 확인 후 경고/차단을 권장합니다.
var redirectUri = ProjectVG.Infrastructure.Auth.OAuth2.Config.ServerOAuth2Config.Instance.GetCurrentPlatformRedirectUri(); var uri = new Uri(redirectUri); + if (!uri.IsLoopback) + Debug.LogWarning($"[DesktopCallbackHandler] 루프백이 아닌 호스트가 설정됨: {uri.Host}. 로컬 콜백 서버는 루프백 사용을 권장합니다."); // HttpListener는 pathless 프리픽스를 권장(하위 경로 허용) var prefix = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
175-201
: 응답 헤더에 캐시 방지 추가(성공 페이지)민감 파라미터가 포함된 리다이렉트 직후 페이지는 캐시 금지 헤더를 권장합니다.
response.StatusCode = (int)HttpStatusCode.OK; + response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "0";
209-235
: 응답 헤더에 캐시 방지 추가(실패 페이지)동일하게 캐시 금지 적용을 권장합니다.
response.StatusCode = (int)HttpStatusCode.BadRequest; + response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "0";
241-262
: 기본 페이지도 캐시 금지 권장디버깅 중 파라미터 잔존을 방지합니다.
response.StatusCode = (int)HttpStatusCode.OK; + response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "0";
279-291
: 포커스 이벤트 의도와 WaitForCallbackAsync 변경 정합성 확인WaitForCallbackAsync를 비블로킹으로 바꾸면 _isWaitingForCallback의 수명이 Initialize~Cleanup 전반으로 가는 게 자연스럽습니다. Initialize에서 true, Cleanup에서 false로 관리하도록 위 변경과 함께 일관성 확인 바랍니다.
296-305
: CheckForCallbackUrl는 현재 로그만 출력기능적 역할이 없으므로 노이즈입니다. 필요 없다면 제거하거나, 포커스 복귀 시 상태 점검/진단(리스너 상태, 바인딩 프리픽스 등)을 구체화하세요.
315-333
: System.Web.HttpUtility 사용의 플랫폼 호환성 확인IL2CPP/트리밍 환경에서 System.Web 의존성은 빌드 환경에 따라 제외될 수 있습니다. 파싱만 필요하므로 간단한 쿼리 파서(URI 분해 + WWWForm/UnityWebRequest 유틸)로 대체 가능 여부 검토 바랍니다.
Assets/Core/Audio/AudioRecorder.cs (3)
40-45
: IsRecordingAvailable의 UNITY_WEBGL 분기는 도달 불가 — 단순화 권장.상단 가드 때문에 이 분기는 컴파일되지 않습니다. 프로퍼티를 단순화해 가독성과 유지보수성을 높이세요.
- public bool IsRecordingAvailable => -#if UNITY_WEBGL && !UNITY_EDITOR - false; -#else - Microphone.devices.Length > 0; -#endif + public bool IsRecordingAvailable => Microphone.devices.Length > 0;
107-115
: StartRecording의 UNITY_WEBGL 조기 반환 분기(dead code) 제거.상단 가드로 인해 여기 UNITY_WEBGL && !UNITY_EDITOR 분기는 절대 컴파일되지 않습니다. 제거하여 단순화하세요.
-#if UNITY_WEBGL && !UNITY_EDITOR - Debug.LogWarning("[AudioRecorder] WebGL에서는 마이크로폰이 지원되지 않습니다."); - OnError?.Invoke("WebGL에서는 음성 녹음이 지원되지 않습니다."); - _isRecording = false; - return false; -#else _recordingClip = Microphone.Start(_currentDevice ?? string.Empty, false, _maxRecordingLength, _sampleRate); -#endif의도 확인: 에디터에서 WebGL 타깃으로 플레이할 때도 마이크를 허용하려는 것인지(현 코드처럼), 에디터-웹GL에서도 막으려는 것인지(그럴 경우 상단 가드 전략을 재조정해야 함) 확인 부탁드립니다.
151-156
: 실제 녹음 길이 산정 시 시스템 시간 대신 Microphone.GetPosition 사용 권장.현재 Time.time 차이를 사용합니다. 장치/플랫폼별 레이턴시나 프레임 타이밍에 따라 오차가 날 수 있습니다. End 전에 GetPosition으로 샘플 수를 얻은 뒤, 그 값으로 실제 길이를 역산하세요.
- _recordingEndTime = Time.time; - float actualRecordingDuration = _recordingEndTime - _recordingStartTime; - - Microphone.End(_currentDevice != null ? _currentDevice : string.Empty); + _recordingEndTime = Time.time; + var devName = _currentDevice ?? string.Empty; + int positionSamples = Microphone.GetPosition(devName); + Microphone.End(devName); + float actualRecordingDuration = positionSamples > 0 + ? positionSamples / (float)_sampleRate + : (_recordingEndTime - _recordingStartTime);이렇게 하면 ProcessRecordingClip 내부의 actualSamples 계산 정확도가 개선됩니다(이미 maxSamples로 클램프하고 있어 부작용은 없음).
Assets/Infrastructure/Network/Configs/NetworkConfig.cs (1)
74-75
: ApplyRuntimeGuard 호출 위치 및 환경 설정 방식 재검토
ApplyRuntimeGuard
가 싱글톤 생성 시점(Instance getter, 74–75행)에서 private static으로 한 번만 호출되어environment
필드를 직접 수정하기 때문에, 이후 런타임 환경 변경이 불가능합니다.CurrentEnvironment
프로퍼티 getter나 외부 환경 설정 클래스로 호출 위치를 옮기거나, setter/전용 메서드를 사용해 유연성을 확보하도록 리팩터링하세요.Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
37-37
: 하드코딩된 API 경로 대신 NetworkConfig 헬퍼 사용 권장현재
api/v1/
접두사가 하드코딩되어 있습니다.NetworkConfig.GetVersionedApiUrl
을 사용하면 더 일관성 있고 유지보수가 쉬워집니다.- return await _httpClient.PostAsync<ChatResponse>($"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); + return await _httpClient.PostAsync<ChatResponse>( + NetworkConfig.GetVersionedApiUrl(CHAT_ENDPOINT), request, requiresAuth: true, cancellationToken: cancellationToken);Assets/Infrastructure/Auth/Services/GuestAuthService.cs (1)
187-195
: 토큰 마스킹 메서드 재사용성 개선
MaskDeviceId
메서드가 디바이스 ID뿐만 아니라 토큰 마스킹에도 사용됩니다. 메서드 이름을 더 범용적으로 변경하는 것이 좋습니다.-private string MaskDeviceId(string deviceId) +private string MaskSensitiveData(string data) { - if (string.IsNullOrEmpty(deviceId) || deviceId.Length < 8) + if (string.IsNullOrEmpty(data) || data.Length < 8) { return "***"; } - return $"{deviceId.Substring(0, 4)}****{deviceId.Substring(deviceId.Length - 4)}"; + return $"{data.Substring(0, 4)}****{data.Substring(data.Length - 4)}"; }그리고 호출 부분도 업데이트:
-Debug.Log($"[GuestAuthService] 디바이스 ID 생성: {MaskDeviceId(deviceId)}"); +Debug.Log($"[GuestAuthService] 디바이스 ID 생성: {MaskSensitiveData(deviceId)}");Assets/Infrastructure/Auth/TokenRefreshService.cs (1)
163-165
: 하드코딩된 API 경로 대신 NetworkConfig 사용 권장일관성을 위해
NetworkConfig.GetAuthApiUrl("refresh")
를 사용하는 것이 좋습니다.var response = await httpClient.PostAsync<ServerOAuth2TokenResponse>( - "api/v1/auth/refresh", + NetworkConfig.GetAuthApiUrl("refresh"), refreshRequest, requiresAuth: false // 토큰 갱신은 인증 불필요 );Assets/Infrastructure/Network/Http/HttpApiClient.cs (1)
485-502
: 응답 헤더 추출 로직 개선헤더 이름들이 하드코딩되어 있습니다. 설정 가능하도록 개선하면 유연성이 높아집니다.
+private static readonly string[] ResponseHeadersToExtract = new[] +{ + "X-Access-Token", "X-Refresh-Token", "X-Expires-In", "X-User-Id", + "Content-Type", "Authorization" +}; + private Dictionary<string, string> ExtractResponseHeaders(UnityWebRequest request) { var headers = new Dictionary<string, string>(); - var headerNames = new[] - { - "X-Access-Token", "X-Refresh-Token", "X-Expires-In", "X-User-Id", - "Content-Type", "Authorization" - }; - foreach (var headerName in headerNames) + foreach (var headerName in ResponseHeadersToExtract) { var headerValue = request.GetResponseHeader(headerName); if (!string.IsNullOrEmpty(headerValue)) headers[headerName] = headerValue; } return headers; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
Assets/App/Scenes/MainScene.unity
is excluded by!**/*.unity
📒 Files selected for processing (18)
Assets/Core/Audio/AudioRecorder.cs
(5 hunks)Assets/Core/DebugConsole/GameDebugConsoleManager.cs
(4 hunks)Assets/Domain/Character/Script/CharacterActionController.cs
(5 hunks)Assets/Infrastructure/Auth/AuthManager.cs
(1 hunks)Assets/Infrastructure/Auth/JwtTokenParser.cs
(1 hunks)Assets/Infrastructure/Auth/Models/AccessToken.cs
(1 hunks)Assets/Infrastructure/Auth/Models/TokenSet.cs
(1 hunks)Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs
(1 hunks)Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs
(1 hunks)Assets/Infrastructure/Auth/Services/GuestAuthService.cs
(1 hunks)Assets/Infrastructure/Auth/TokenManager.cs
(1 hunks)Assets/Infrastructure/Auth/TokenRefreshService.cs
(1 hunks)Assets/Infrastructure/Network/Configs/NetworkConfig.cs
(7 hunks)Assets/Infrastructure/Network/DTOs/Auth/GuestLoginResponse.cs
(1 hunks)Assets/Infrastructure/Network/Http/HttpApiClient.cs
(7 hunks)Assets/Infrastructure/Network/Services/CharacterApiService.cs
(6 hunks)Assets/Infrastructure/Network/Services/ChatApiService.cs
(2 hunks)Assets/Infrastructure/Network/Services/STTService.cs
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- Assets/Infrastructure/Auth/Models/AccessToken.cs
- Assets/Infrastructure/Network/Services/STTService.cs
- Assets/Infrastructure/Auth/TokenManager.cs
- Assets/Infrastructure/Auth/AuthManager.cs
- Assets/Infrastructure/Network/DTOs/Auth/GuestLoginResponse.cs
- Assets/Infrastructure/Network/Services/CharacterApiService.cs
- Assets/Infrastructure/Auth/JwtTokenParser.cs
- Assets/Infrastructure/Auth/Models/TokenSet.cs
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-27T01:15:29.735Z
Learnt from: ImGdevel
PR: ProjectVG/ProjectVG-Client#8
File: Assets/Core/DebugConsole/GameDebugConsoleManager.cs:106-108
Timestamp: 2025-08-27T01:15:29.735Z
Learning: ImGdevel은 개발/배포 단계에서 첫 로그를 확인하기 위해 GameDebugConsoleManager에서 콘솔을 초기에 표시되도록 설정하는 것을 선호한다.
Applied to files:
Assets/Core/DebugConsole/GameDebugConsoleManager.cs
📚 Learning: 2025-08-21T15:18:26.828Z
Learnt from: CR
PR: ProjectVG/ProjectVG-Client#0
File: Docs/Conventions/CodeStyle_CSharp.md:0-0
Timestamp: 2025-08-21T15:18:26.828Z
Learning: Applies to Docs/Conventions/**/*.cs : Namespaces should mirror folder structure and start with ProjectVG (e.g., ProjectVG.Domain.Chat)
Applied to files:
Assets/Infrastructure/Network/Configs/NetworkConfig.cs
🧬 Code graph analysis (6)
Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs (4)
Assets/Infrastructure/Auth/OAuth2/Handlers/OAuth2CallbackHandlerFactory.cs (1)
IOAuth2CallbackHandler
(15-37)Assets/Infrastructure/Auth/OAuth2/ServerOAuth2Provider.cs (7)
Task
(66-90)Task
(97-154)Task
(161-207)Task
(214-304)Task
(311-352)Task
(384-437)Task
(444-498)Assets/Infrastructure/Auth/OAuth2/Handlers/IOAuth2CallbackHandler.cs (3)
Task
(17-17)Task
(23-23)Cleanup
(28-28)Assets/Infrastructure/Auth/OAuth2/Config/ServerOAuth2Config.cs (2)
ServerOAuth2Config
(167-184)GetCurrentPlatformRedirectUri
(55-71)
Assets/Infrastructure/Auth/Services/GuestAuthService.cs (5)
Assets/Infrastructure/Network/Http/HttpApiClient.cs (6)
HttpApiClient
(15-510)Awake
(30-33)Initialize
(47-55)UniTask
(100-107)UniTask
(112-119)UniTask
(124-132)Assets/Infrastructure/Auth/TokenManager.cs (5)
TokenManager
(11-354)TokenSet
(110-125)Awake
(44-61)SaveTokens
(63-108)Serializable
(356-362)Assets/Infrastructure/Network/DTOs/Auth/GuestLoginResponse.cs (5)
TokenSet
(40-65)GetDebugInfo
(70-92)Serializable
(10-93)Serializable
(98-130)Serializable
(135-167)Assets/Infrastructure/Auth/Models/TokenSet.cs (5)
TokenSet
(26-29)TokenSet
(31-36)GetDebugInfo
(111-148)HasRefreshToken
(49-52)Serializable
(8-149)Assets/Infrastructure/Auth/Utils/DeviceIdProvider.cs (6)
DeviceIdProvider
(10-237)GetDeviceId
(19-43)MaskDeviceId
(192-201)GetDebugInfo
(227-236)ClearDeviceId
(206-212)GetPlatformInfo
(217-222)
Assets/Infrastructure/Auth/TokenRefreshService.cs (6)
Assets/Infrastructure/Auth/TokenManager.cs (6)
TokenManager
(11-354)Awake
(44-61)IsRefreshTokenExpired
(152-155)GetRefreshToken
(142-145)SaveTokens
(63-108)TokenSet
(110-125)Assets/Infrastructure/Auth/OAuth2/ServerOAuth2Provider.cs (2)
ServerOAuth2Provider
(21-521)ServerOAuth2Provider
(28-50)Assets/Infrastructure/Network/Http/HttpApiClient.cs (18)
Awake
(30-33)Initialize
(47-55)UniTask
(100-107)UniTask
(112-119)UniTask
(124-132)UniTask
(134-142)UniTask
(144-152)UniTask
(154-161)UniTask
(163-173)UniTask
(175-182)UniTask
(184-192)UniTask
(265-279)UniTask
(281-299)UniTask
(301-321)UniTask
(340-365)UniTask
(367-380)UniTask
(382-385)HttpApiClient
(15-510)Assets/Infrastructure/Auth/Models/TokenSet.cs (4)
IsRefreshTokenExpired
(57-60)TokenSet
(26-29)TokenSet
(31-36)HasRefreshToken
(49-52)Assets/Infrastructure/Auth/Models/AccessToken.cs (2)
AccessToken
(11-11)AccessToken
(13-18)Assets/Infrastructure/Auth/Models/RefreshToken.cs (3)
RefreshToken
(12-12)RefreshToken
(14-19)RefreshToken
(21-26)
Assets/Infrastructure/Network/Http/HttpApiClient.cs (2)
Assets/Infrastructure/Auth/TokenManager.cs (2)
TokenManager
(11-354)GetAccessToken
(127-140)Assets/Infrastructure/Network/Configs/NetworkConfig.cs (1)
NetworkConfig
(235-268)
Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs (1)
Assets/Infrastructure/Auth/OAuth2/Models/ServerOAuth2Models.cs (3)
OAuth2CallbackResult
(105-108)OAuth2CallbackResult
(113-121)OAuth2CallbackResult
(126-134)
Assets/Domain/Character/Script/CharacterActionController.cs (3)
Assets/Domain/Character/Script/CharacterModelLoader.cs (2)
Live2DModelConfig
(77-84)Initialize
(33-37)Assets/Domain/Chat/Model/CharacterActionData.cs (1)
CharacterActionType
(43-55)Assets/Domain/Character/Script/CharacterManager.cs (2)
Initialize
(34-56)PlayAction
(115-121)
🔇 Additional comments (7)
Assets/Domain/Character/Script/CharacterActionController.cs (1)
169-189
: 이전 레이스 이슈 해결: 새 액션 전에 코루틴 전량 정리한 점 좋습니다
PlayAction
시작부에서 항상StopAllMotionCoroutines()
호출하여 이전 대기 코루틴 정리합니다. Idle↔Non-Idle, Non-Idle↔Non-Idle 전환 경쟁 상태 완화에 효과적입니다.Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs (2)
27-27
: IsSupported 구현 LGTM플랫폼 가드가 반영되었습니다.
108-116
: 리다이렉트 URI 기반 Prefix 바인딩 LGTM하드코딩 제거되어 설정과 정합성이 좋아졌습니다.
Assets/Core/Audio/AudioRecorder.cs (1)
179-181
: OnRecordingStopped를 finally에서 단 한 번만 발생시키는 구조는 적절합니다.정상/에러/예외 경로 모두에서 중복 호출을 방지합니다. “녹음 중이 아님”의 초기 반환 경로에서는 호출되지 않는 것도 의도에 부합해 보입니다.
Assets/Infrastructure/Network/Configs/NetworkConfig.cs (3)
181-181
: 세션 ID 이스케이프 처리 추가
Uri.EscapeDataString
을 사용하여 세션 ID를 안전하게 URL 인코딩하는 것은 좋은 변경입니다. XSS 및 인젝션 공격을 방지합니다.
213-217
: API URL 빌더 메서드 추가 및 네이밍 명확화새로운 URL 빌더 메서드들이 추가되었습니다:
GetFullApiUrl
:/api/v1
접두사 없이 전체 URL 생성GetVersionedApiUrl
:/api/v1
접두사 포함된 URL 생성GetAuthApiUrl
: 인증 관련 엔드포인트용메서드 이름이 명확하고 용도가 잘 구분되어 있습니다.
Also applies to: 220-224, 229-229
124-125
: dev/test 환경은 HTTP 사용이 의도된 동작입니다.
현재developmentServer
와testServer
가localhost:7900
으로 설정되어 있어 HTTP 스킴을 사용하며, 테스트 환경에서 SSL이 필요하면testServer
값을https://…
주소로 변경해 주세요.
@@ -1,4 +1,5 @@ | |||
#nullable enable | |||
#if !UNITY_WEBGL || UNITY_EDITOR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
전처리 가드 전략 정리 필요 (#if !UNITY_WEBGL || UNITY_EDITOR).
이 가드로 인해 WebGL 실제 빌드(!UNITY_EDITOR)에서는 클래스 전체가 제외됩니다. 그 결과, 파일 내부의 UNITY_WEBGL 분기(예: IsRecordingAvailable, StartRecording 내부)는 절대 컴파일/실행되지 않아 “죽은 코드”가 됩니다. 두 가지 중 하나로 단순화하세요:
- 전략 A(현 상태 유지): 상단 가드 유지 + 내부 UNITY_WEBGL 분기 제거.
- 전략 B(API 안정성): 상단 가드를 없애고, 대신 WebGL 전용 “no-op 스텁” 클래스를 별도 파일로 제공하여 호출부의 조건부 컴파일을 줄임.
WebGL 빌드에서 AudioRecorder를 참조하는 코드가 모두 전처리 가드로 보호되는지 점검해 주세요. 아래 스크립트로 대략적인 사용처를 스캔할 수 있습니다.
전략 B를 택할 경우 예시(새 파일 추가: Assets/Core/Audio/AudioRecorder.WebGLStub.cs):
#if UNITY_WEBGL && !UNITY_EDITOR
#nullable enable
using System;
using UnityEngine;
namespace ProjectVG.Core.Audio {
public class AudioRecorder : Singleton<AudioRecorder> {
public event Action? OnRecordingStarted;
public event Action? OnRecordingStopped;
public event Action<AudioClip>? OnRecordingCompleted;
public event Action<string>? OnError;
public event Action<float>? OnRecordingProgress;
public bool IsRecording => false;
public float RecordingDuration => 0f;
public bool IsRecordingAvailable => false;
public float RecordingProgress => 0f;
public bool StartRecording() {
Debug.LogWarning("[AudioRecorder] WebGL에서는 음성 녹음이 지원되지 않습니다.");
OnError?.Invoke("WebGL에서는 음성 녹음이 지원되지 않습니다.");
return false;
}
public AudioClip? StopRecording() { OnRecordingStopped?.Invoke(); return null; }
public byte[] AudioClipToWavBytes(AudioClip audioClip) => Array.Empty<byte>();
public bool SaveRecordingToFile(AudioClip audioClip, string fileName = "recording") => false;
public string[] GetAvailableMicrophones() => Array.Empty<string>();
public string GetDefaultMicrophone() => string.Empty;
public void SetMicrophone(string deviceName) { }
}
}
#endif
🏁 Script executed:
#!/bin/bash
# AudioRecorder 참조 위치와 주변 전처리 가드 맥락 확인
rg -nP -C3 '\bAudioRecorder\b' -g '!**/AudioRecorder.cs' -S
Length of output: 2065
🏁 Script executed:
#!/bin/bash
# VoiceInputView.cs 상단에 전처리 지시문 사용 여부 확인
sed -n '1,100p' Assets/Domain/Chat/View/VoiceInputView.cs
# AudioRecorder 참조부 주변 컨텍스트(전처리 가드) 확인
sed -n '190,260p' Assets/Domain/Chat/View/VoiceInputView.cs
Length of output: 5392
전처리 가드 전략 확정 및 불필요 내부 분기 제거
- 현재
#if !UNITY_WEBGL || UNITY_EDITOR
가드 유지(Strategy A) 선택 시, AudioRecorder 내부의#if UNITY_WEBGL
분기는 모두 삭제 - 또는 상단 가드 제거 후 WebGL 전용 스텁 클래스를 별도 파일(예: AudioRecorder.WebGLStub.cs)로 제공(Strategy B)
- VoiceInputView 등 AudioRecorder 참조부도 동일 가드 하에 있어 WebGL 빌드에서 안전하게 제외됩니다.
🤖 Prompt for AI Agents
In Assets/Core/Audio/AudioRecorder.cs around line 2, the current top
preprocessor guard `#if !UNITY_WEBGL || UNITY_EDITOR` requires you to choose one
strategy and remove redundant internal branches: either (Strategy A) keep this
top-level guard and remove all internal `#if UNITY_WEBGL` / `#else` / `#endif`
blocks inside AudioRecorder so the entire class is excluded from WebGL builds,
or (Strategy B) remove the top-level guard and create a separate WebGL stub file
(e.g., AudioRecorder.WebGLStub.cs) that contains the WebGL-safe minimal
implementations, then ensure any callers (like VoiceInputView) are under the
same guard or reference the stub so WebGL builds compile cleanly.
public async Task InitializeAsync(string expectedState, float timeoutSeconds) | ||
{ | ||
_expectedState = expectedState; | ||
_timeoutSeconds = timeoutSeconds; | ||
_isInitialized = true; | ||
_cancellationTokenSource = new CancellationTokenSource(); | ||
_lastActivityTime = DateTime.UtcNow; | ||
|
||
// Unity 이벤트 등록 | ||
Application.focusChanged += OnApplicationFocusChanged; | ||
|
||
// 로컬 HTTP 서버 시작 | ||
await StartLocalServerAsync(); | ||
|
||
Debug.Log($"[DesktopCallbackHandler] 초기화 완료 - State: {expectedState}, Timeout: {timeoutSeconds}초"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
이벤트 등록 타이밍 조정 및 대기 상태 초기화
StartLocalServerAsync 실패 시 focusChanged 이벤트가 등록된 채로 누수될 수 있습니다. 서버 시작 이후에 등록하고, 대기 상태는 Initialize 시점에 true로 두는 편이 자연스럽습니다(WaitForCallbackAsync를 비블로킹으로 바꿀 경우).
public async Task InitializeAsync(string expectedState, float timeoutSeconds)
{
_expectedState = expectedState;
_timeoutSeconds = timeoutSeconds;
_isInitialized = true;
_cancellationTokenSource = new CancellationTokenSource();
_lastActivityTime = DateTime.UtcNow;
-
- // Unity 이벤트 등록
- Application.focusChanged += OnApplicationFocusChanged;
-
- // 로컬 HTTP 서버 시작
- await StartLocalServerAsync();
-
+ // 로컬 HTTP 서버 시작
+ await StartLocalServerAsync();
+
+ // Unity 이벤트 등록(서버 시작 성공 후)
+ Application.focusChanged += OnApplicationFocusChanged;
+ _isWaitingForCallback = true;
+
Debug.Log($"[DesktopCallbackHandler] 초기화 완료 - State: {expectedState}, Timeout: {timeoutSeconds}초");
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public async Task InitializeAsync(string expectedState, float timeoutSeconds) | |
{ | |
_expectedState = expectedState; | |
_timeoutSeconds = timeoutSeconds; | |
_isInitialized = true; | |
_cancellationTokenSource = new CancellationTokenSource(); | |
_lastActivityTime = DateTime.UtcNow; | |
// Unity 이벤트 등록 | |
Application.focusChanged += OnApplicationFocusChanged; | |
// 로컬 HTTP 서버 시작 | |
await StartLocalServerAsync(); | |
Debug.Log($"[DesktopCallbackHandler] 초기화 완료 - State: {expectedState}, Timeout: {timeoutSeconds}초"); | |
} | |
public async Task InitializeAsync(string expectedState, float timeoutSeconds) | |
{ | |
_expectedState = expectedState; | |
_timeoutSeconds = timeoutSeconds; | |
_isInitialized = true; | |
_cancellationTokenSource = new CancellationTokenSource(); | |
_lastActivityTime = DateTime.UtcNow; | |
// 로컬 HTTP 서버 시작 | |
await StartLocalServerAsync(); | |
// Unity 이벤트 등록(서버 시작 성공 후) | |
Application.focusChanged += OnApplicationFocusChanged; | |
_isWaitingForCallback = true; | |
Debug.Log($"[DesktopCallbackHandler] 초기화 완료 - State: {expectedState}, Timeout: {timeoutSeconds}초"); | |
} |
🤖 Prompt for AI Agents
Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs around
lines 29-44: currently the Unity focusChanged event is registered before
StartLocalServerAsync, which can leak the event handler if server start fails;
change the flow so InitializeAsync sets any "waiting for callback" state (e.g. a
boolean like _isWaiting or similar) to true immediately, then await
StartLocalServerAsync and only after it completes successfully subscribe
Application.focusChanged += OnApplicationFocusChanged; also add a try/catch
around StartLocalServerAsync to ensure that on exception you clean up/reset
_isWaiting and _isInitialized and do not leave the handler registered (or
unsubscribe if previously registered), and keep the Debug.Log after successful
initialization.
public async Task<string> WaitForCallbackAsync() | ||
{ | ||
if (!_isInitialized) | ||
{ | ||
throw new InvalidOperationException("데스크톱 콜백 핸들러가 초기화되지 않았습니다."); | ||
} | ||
|
||
Debug.Log("[DesktopCallbackHandler] OAuth2 콜백 대기 시작"); | ||
_isWaitingForCallback = true; | ||
|
||
var startTime = DateTime.UtcNow; | ||
var timeout = TimeSpan.FromSeconds(_timeoutSeconds); | ||
|
||
while (DateTime.UtcNow - startTime < timeout && !_isDisposed) | ||
{ | ||
if (!string.IsNullOrEmpty(_callbackUrl)) | ||
{ | ||
Debug.Log($"[DesktopCallbackHandler] OAuth2 콜백 수신: {_callbackUrl}"); | ||
_isWaitingForCallback = false; | ||
return _callbackUrl; | ||
} | ||
|
||
// 앱이 포커스를 잃었을 때 더 자주 체크 | ||
var checkInterval = Application.isFocused ? 100 : 50; // 백그라운드일 때 더 빠르게 체크 | ||
await UniTask.Delay(checkInterval); | ||
|
||
// 디버그 정보 출력 (10초마다) | ||
if ((DateTime.UtcNow - _lastActivityTime).TotalSeconds >= 10) | ||
{ | ||
Debug.Log($"[DesktopCallbackHandler] 콜백 대기 중... (경과: {(DateTime.UtcNow - startTime).TotalSeconds:F1}초, 포커스: {Application.isFocused})"); | ||
_lastActivityTime = DateTime.UtcNow; | ||
} | ||
} | ||
|
||
Debug.LogWarning("[DesktopCallbackHandler] OAuth2 콜백 타임아웃"); | ||
_isWaitingForCallback = false; | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WaitForCallbackAsync가 상위 프로바이더 사용 패턴과 불일치(블로킹/이중 타임아웃 유발)
ServerOAuth2Provider.WaitForOAuth2CallbackAsync는 주기적 폴링(≈100–200ms)로 빠르게 반환되는 핸들러를 기대합니다. 현재 구현은 자체 타임아웃 루프를 돌며 블로킹되어 상위 루프가 제대로 동작하지 않습니다. 즉, 이중 타임아웃·중복 대기 및 UX 저하가 발생합니다.
폴링형(비블로킹)으로 단순화해주세요.
- public async Task<string> WaitForCallbackAsync()
- {
- if (!_isInitialized)
- {
- throw new InvalidOperationException("데스크톱 콜백 핸들러가 초기화되지 않았습니다.");
- }
-
- Debug.Log("[DesktopCallbackHandler] OAuth2 콜백 대기 시작");
- _isWaitingForCallback = true;
-
- var startTime = DateTime.UtcNow;
- var timeout = TimeSpan.FromSeconds(_timeoutSeconds);
-
- while (DateTime.UtcNow - startTime < timeout && !_isDisposed)
- {
- if (!string.IsNullOrEmpty(_callbackUrl))
- {
- Debug.Log($"[DesktopCallbackHandler] OAuth2 콜백 수신: {_callbackUrl}");
- _isWaitingForCallback = false;
- return _callbackUrl;
- }
-
- var checkInterval = Application.isFocused ? 100 : 50;
- await UniTask.Delay(checkInterval);
-
- if ((DateTime.UtcNow - _lastActivityTime).TotalSeconds >= 10)
- {
- Debug.Log($"[DesktopCallbackHandler] 콜백 대기 중... (경과: {(DateTime.UtcNow - startTime).TotalSeconds:F1}초, 포커스: {Application.isFocused})");
- _lastActivityTime = DateTime.UtcNow;
- }
- }
-
- Debug.LogWarning("[DesktopCallbackHandler] OAuth2 콜백 타임아웃");
- _isWaitingForCallback = false;
- return null;
- }
+ public Task<string> WaitForCallbackAsync()
+ {
+ if (!_isInitialized)
+ throw new InvalidOperationException("데스크톱 콜백 핸들러가 초기화되지 않았습니다.");
+
+ // 폴링 시 즉시 반환 (상위에서 주기적 호출)
+ var url = Volatile.Read(ref _callbackUrl);
+ return Task.FromResult(url);
+ }
Committable suggestion skipped: line range outside the PR's diff.
Debug.Log($"[DesktopCallbackHandler] OAuth2 콜백 파라미터 - State: {state}, Success: {success}"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
민감 파라미터(state) 평문 로그 노출
state는 CSRF 토큰으로 간주되어야 합니다. 현재 로그에 원문이 노출됩니다. 마스킹하세요.
- Debug.Log($"[DesktopCallbackHandler] OAuth2 콜백 파라미터 - State: {state}, Success: {success}");
+ var maskedState = string.IsNullOrEmpty(state) ? state : "***";
+ Debug.Log($"[DesktopCallbackHandler] OAuth2 콜백 파라미터 - State: {maskedState}, Success: {success}");
🤖 Prompt for AI Agents
In Assets/Infrastructure/Auth/OAuth2/Handlers/DesktopCallbackHandler.cs around
lines 170-171, the Debug.Log prints the raw OAuth2 state (a CSRF token) in
plaintext; replace this with a masked or hashed representation instead of the
raw value. Change the log to compute either a one-way hash (e.g., SHA-256) of
state or a masked snippet (e.g., first 4 + "..." + last 4 or fixed "****" for
short values) and log that masked/hash value along with Success; do not log the
full state anywhere and ensure any helper used for hashing/masking is
deterministic and does not expose the token.
public static bool IsOAuth2Callback(string url) | ||
{ | ||
if (string.IsNullOrEmpty(url)) | ||
{ | ||
return false; | ||
} | ||
|
||
try | ||
{ | ||
var uri = new Uri(url); | ||
var query = HttpUtility.ParseQueryString(uri.Query); | ||
|
||
// success 파라미터가 있으면 OAuth2 콜백으로 간주 | ||
return query.AllKeys.Contains("success"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
IsOAuth2Callback: 케이스 인식 및 System.Web 제거
AllKeys.Contains("success")
는 케이스 민감합니다. 내부 파서로 변경하고 OrdinalIgnoreCase 딕셔너리를 사용하세요.
- var uri = new Uri(url);
- var query = HttpUtility.ParseQueryString(uri.Query);
-
- // success 파라미터가 있으면 OAuth2 콜백으로 간주
- return query.AllKeys.Contains("success");
+ var uri = new Uri(url);
+ var dict = ParseQueryToDictionary(uri.Query);
+ return dict.ContainsKey("success");
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static bool IsOAuth2Callback(string url) | |
{ | |
if (string.IsNullOrEmpty(url)) | |
{ | |
return false; | |
} | |
try | |
{ | |
var uri = new Uri(url); | |
var query = HttpUtility.ParseQueryString(uri.Query); | |
// success 파라미터가 있으면 OAuth2 콜백으로 간주 | |
return query.AllKeys.Contains("success"); | |
} | |
public static bool IsOAuth2Callback(string url) | |
{ | |
if (string.IsNullOrEmpty(url)) | |
{ | |
return false; | |
} | |
try | |
{ | |
var uri = new Uri(url); | |
var dict = ParseQueryToDictionary(uri.Query); | |
return dict.ContainsKey("success"); |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Auth/OAuth2/Utils/OAuth2CallbackParser.cs around lines
193 to 207, the current IsOAuth2Callback implementation uses
HttpUtility.ParseQueryString and query.AllKeys.Contains("success") which is
case-sensitive and depends on System.Web; replace the parse step with an
internal query-string parser that produces a Dictionary<string,string> using
StringComparer.OrdinalIgnoreCase, populate it by splitting uri.Query on '&' and
'=' (handle leading '?' and URL-decode keys/values, ignore empty entries), then
return dict.ContainsKey("success") (also guard for null/empty url and exceptions
as before).
private void EnsureAuthToken(bool requiresAuth) | ||
{ | ||
if (!requiresAuth) return; | ||
|
||
try | ||
{ | ||
var tokenManager = ProjectVG.Infrastructure.Auth.TokenManager.Instance; | ||
var accessToken = tokenManager.GetAccessToken(); | ||
|
||
Debug.Log($"[HttpApiClient] Access Token: {accessToken}"); | ||
|
||
if (!string.IsNullOrEmpty(accessToken)) | ||
{ | ||
SetAuthToken(accessToken); | ||
} | ||
else | ||
{ | ||
Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다."); | ||
RemoveDefaultHeader(AUTHORIZATION_HEADER); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
Debug.LogError($"[HttpApiClient] 토큰 설정 실패: {ex.Message}"); | ||
RemoveDefaultHeader(AUTHORIZATION_HEADER); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
토큰 획득 실패 시 예외 처리 개선 필요
EnsureAuthToken
메서드에서 토큰 획득 실패 시 단순히 로그만 남기고 계속 진행합니다. requiresAuth=true
인 경우 명시적인 예외를 던지는 것이 좋습니다.
private void EnsureAuthToken(bool requiresAuth)
{
if (!requiresAuth) return;
try
{
var tokenManager = ProjectVG.Infrastructure.Auth.TokenManager.Instance;
var accessToken = tokenManager.GetAccessToken();
- Debug.Log($"[HttpApiClient] Access Token: {accessToken}");
-
if (!string.IsNullOrEmpty(accessToken))
{
SetAuthToken(accessToken);
}
else
{
- Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다.");
- RemoveDefaultHeader(AUTHORIZATION_HEADER);
+ RemoveDefaultHeader(AUTHORIZATION_HEADER);
+ throw new UnauthorizedAccessException("[HttpApiClient] 인증이 필요하지만 유효한 Access Token을 찾을 수 없습니다.");
}
}
catch (Exception ex)
{
- Debug.LogError($"[HttpApiClient] 토큰 설정 실패: {ex.Message}");
RemoveDefaultHeader(AUTHORIZATION_HEADER);
+ Debug.LogError($"[HttpApiClient] 토큰 설정 실패: {ex.Message}");
+ throw new UnauthorizedAccessException($"[HttpApiClient] 토큰 설정 실패: {ex.Message}", ex);
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private void EnsureAuthToken(bool requiresAuth) | |
{ | |
if (!requiresAuth) return; | |
try | |
{ | |
var tokenManager = ProjectVG.Infrastructure.Auth.TokenManager.Instance; | |
var accessToken = tokenManager.GetAccessToken(); | |
Debug.Log($"[HttpApiClient] Access Token: {accessToken}"); | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
SetAuthToken(accessToken); | |
} | |
else | |
{ | |
Debug.LogWarning("[HttpApiClient] 유효한 Access Token을 찾을 수 없습니다."); | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
} | |
} | |
catch (Exception ex) | |
{ | |
Debug.LogError($"[HttpApiClient] 토큰 설정 실패: {ex.Message}"); | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
} | |
} | |
private void EnsureAuthToken(bool requiresAuth) | |
{ | |
if (!requiresAuth) return; | |
try | |
{ | |
var tokenManager = ProjectVG.Infrastructure.Auth.TokenManager.Instance; | |
var accessToken = tokenManager.GetAccessToken(); | |
// 민감 정보 노출 제거: accessToken 직접 로그 출력 삭제 | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
SetAuthToken(accessToken); | |
} | |
else | |
{ | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
throw new UnauthorizedAccessException("[HttpApiClient] 인증이 필요하지만 유효한 Access Token을 찾을 수 없습니다."); | |
} | |
} | |
catch (Exception ex) | |
{ | |
RemoveDefaultHeader(AUTHORIZATION_HEADER); | |
Debug.LogError($"[HttpApiClient] 토큰 설정 실패: {ex.Message}"); | |
throw new UnauthorizedAccessException($"[HttpApiClient] 토큰 설정 실패: {ex.Message}", ex); | |
} | |
} |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/Http/HttpApiClient.cs around lines 72 to 98,
EnsureAuthToken currently only logs when token acquisition fails or returns
empty and continues; update it so that when requiresAuth is true you throw a
clear exception instead of silently proceeding: propagate the original exception
(as inner) when tokenManager.GetAccessToken() throws, and if GetAccessToken()
returns null/empty and requiresAuth is true, throw an InvalidOperationException
(or custom exception) with a message indicating authentication is required;
still remove the authorization header on failure and include the original
exception/message as inner details so callers can handle or surface the failure.
if (kvp.Value is byte[] byteData) | ||
{ | ||
string fileName = fileNames.ContainsKey(kvp.Key) ? fileNames[kvp.Key] : DEFAULT_FILE_NAME; | ||
form.AddBinaryData(kvp.Key, byteData, fileName); | ||
} | ||
else | ||
{ | ||
form.AddField(kvp.Key, kvp.Value?.ToString() ?? string.Empty); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Form 데이터 바이너리 null 체크 추가
byteData
가 null인 경우 AddBinaryData
호출 시 문제가 발생할 수 있습니다.
if (kvp.Value is byte[] byteData)
{
+ if (byteData == null || byteData.Length == 0)
+ {
+ Debug.LogWarning($"[HttpApiClient] 빈 바이너리 데이터: {kvp.Key}");
+ continue;
+ }
string fileName = fileNames.ContainsKey(kvp.Key) ? fileNames[kvp.Key] : DEFAULT_FILE_NAME;
form.AddBinaryData(kvp.Key, byteData, fileName);
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (kvp.Value is byte[] byteData) | |
{ | |
string fileName = fileNames.ContainsKey(kvp.Key) ? fileNames[kvp.Key] : DEFAULT_FILE_NAME; | |
form.AddBinaryData(kvp.Key, byteData, fileName); | |
} | |
else | |
{ | |
form.AddField(kvp.Key, kvp.Value?.ToString() ?? string.Empty); | |
} | |
if (kvp.Value is byte[] byteData) | |
{ | |
// 바이너리 데이터가 없거나 비어 있으면 스킵 | |
if (byteData == null || byteData.Length == 0) | |
{ | |
Debug.LogWarning($"[HttpApiClient] 빈 바이너리 데이터: {kvp.Key}"); | |
continue; | |
} | |
string fileName = fileNames.ContainsKey(kvp.Key) | |
? fileNames[kvp.Key] | |
: DEFAULT_FILE_NAME; | |
form.AddBinaryData(kvp.Key, byteData, fileName); | |
} | |
else | |
{ | |
form.AddField(kvp.Key, kvp.Value?.ToString() ?? string.Empty); | |
} |
🤖 Prompt for AI Agents
In Assets/Infrastructure/Network/Http/HttpApiClient.cs around lines 393 to 401,
the code assumes kvp.Value as byte[] is non-null before calling
form.AddBinaryData; add a null check for byteData and avoid calling
AddBinaryData when byteData is null (either skip the entry or fall back to
adding an empty field or a zero-length byte array with a filename), e.g., check
if byteData != null then call AddBinaryData else call form.AddField(kvp.Key,
string.Empty) (or skip) to prevent null reference issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs (1)
45-47
: AudioFormat을 null 불가로 정의해 널 안전성 보장.디폴트가
"wav"
라면 타입을 nullable로 둘 이유가 없습니다. 역직렬화 시 null이라도 기본값으로 안전하게 유지됩니다.- [JsonProperty("audio_format")] - public string? AudioFormat { get; set; } = "wav"; + [JsonProperty("audio_format")] + public string AudioFormat { get; set; } = "wav";Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs (1)
1-551
: Console.WriteLine 및 토큰 로그 노출 제거
Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs:111–112
•Console.WriteLine
호출을 Unity 로그 API(Debug.Log
/Debug.LogWarning
/Debug.LogError
)로 대체
• 토큰(AccessToken
/RefreshToken
) 내용 출력 제거 또는 반드시 마스킹 처리Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs:143
•ConnectAsync
에서wsUrl
직접 로그 출력 시 URL 쿼리의 토큰이 그대로 노출됨
• 로그용 URL은 토큰을 제거하거나SanitizeWsUrlForLogs
같은 마스킹 로직 적용WebGL용
DispatchMessageQueue
호출은 이미
#if !UNITY_WEBGL || UNITY_EDITOR
로 감싸져 있어 추가 조치 불필요
♻️ Duplicate comments (4)
Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
32-38
: null 요청 가드 추가 필요 (재지적).
request
가 null이면 내부 직렬화/HTTP 호출에서 즉시 NRE가 납니다. 메서드 초입에서 명시적으로 가드해 주세요.public async UniTask<WebSocketResponse> SendChatAsync(ChatRequest request, CancellationToken cancellationToken = default) { ValidateHttpClient(); + if (request == null) + throw new ArgumentNullException(nameof(request)); - return await _httpClient.PostAsync<WebSocketResponse>($"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); + return await _httpClient.PostAsync<WebSocketResponse>($"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); }Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs (3)
62-67
: WebGL에서 DispatchMessageQueue 전처리기 조건이 반대로 되어 런타임에서 메시지 처리가 누락됩니다실제 WebGL 빌드(에디터 제외)에서만 DispatchMessageQueue를 호출해야 합니다. 현재 조건은 반대입니다.
-#if !UNITY_WEBGL || UNITY_EDITOR - // NativeWebSocket의 메시지 큐 처리 (WebGL 제외) - _nativeWebSocket?.DispatchMessageQueue(); -#endif +#if UNITY_WEBGL && !UNITY_EDITOR + // WebGL 빌드에서 메시지 큐 처리 + _nativeWebSocket?.DispatchMessageQueue(); +#endif참고: Platforms/WebGLWebSocket.cs도 동일 수정 필요합니다.
111-113
: 토큰을 URL 쿼리에 넣고 로그로 노출함 — 보안 리스크 높음
- JWT를 URL 쿼리에 싣거나 부분이라도 로그로 출력하면 프록시/접근 로그에 남습니다.
- 최소한 URL 로깅을 마스킹/정규화하고, 토큰은 URL 대신 헤더(Authorization: Bearer ...) 또는 Sec-WebSocket-Protocol 사용으로 이전하세요. 현재 구조상 즉시 헤더 주입이 어렵다면, 임시로 URL 인코딩·로그 마스킹은 필수입니다.
필수 최소 패치:
- Console.WriteLine($"[WebSocket] ConnectAsync 호출 - 연결 상태: {_isConnected}, 연결 중: {_isConnecting}"); - Console.WriteLine($"[WebSocket] 토큰 상태 - AccessToken: {_tokenManager.GetAccessToken()?.Substring(0, 10) ?? "null"}, RefreshToken: {_tokenManager.GetRefreshToken()?.Substring(0, 10) ?? "null"}"); + Debug.Log($"[WebSocket] ConnectAsync 호출 - 연결 상태: {_isConnected}, 연결 중: {_isConnecting}"); + Debug.Log($"[WebSocket] 토큰 상태 확인 완료"); - var wsUrl = GetWebSocketUrlWithToken(); + var wsUrl = GetWebSocketUrlWithToken(); - Debug.Log($"[WebSocket] 연결 시도 URL: {wsUrl.Substring(0, Math.Min(wsUrl.Length, 100))}..."); + Debug.Log($"[WebSocket] 연결 시도 URL: {SanitizeWsUrlForLogs(wsUrl)}"); - string accessToken = _tokenManager.GetAccessToken(); + string accessToken = _tokenManager.GetAccessToken(); - if (!string.IsNullOrEmpty(accessToken)) - { - return $"{baseUrl}?token={accessToken}"; - } + if (!string.IsNullOrEmpty(accessToken)) + { + var sep = baseUrl.Contains("?") ? "&" : "?"; + var tokenParam = Uri.EscapeDataString(accessToken); + return $"{baseUrl}{sep}token={tokenParam}"; + }추가(클래스 내부 임의 위치, 메서드 하단 권장):
private static string SanitizeWsUrlForLogs(string url) { if (string.IsNullOrEmpty(url)) return url; try { var u = new Uri(url); var query = u.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < query.Length; i++) { if (query[i].StartsWith("token=", StringComparison.OrdinalIgnoreCase)) query[i] = "token=***"; } var ub = new UriBuilder(u) { Query = string.Join("&", query) }; return ub.Uri.ToString(); } catch { return url; } }권장(후속): INativeWebSocket 및 플랫폼 구현에 헤더/서브프로토콜 인자 추가 → URL에서 토큰 제거. 원하시면 인터페이스 시그니처 및 각 플랫폼별 구현까지 포함한 패치 제안 드리겠습니다.
Also applies to: 143-147, 266-277
524-548
: 로그아웃 후 자동 재연결이 영구 비활성화될 수 있음 — 재로그인 시 복원 필요OnTokensCleared에서
_autoReconnect = false
로 껐지만, OnTokensUpdated에서 복원하지 않아 재로그인 후에도 재연결이 꺼진 상태가 지속될 수 있습니다.private void OnTokensUpdated(ProjectVG.Infrastructure.Auth.Models.TokenSet tokenSet) { Debug.Log("[WebSocket] 토큰이 업데이트되었습니다. 연결을 시도합니다."); + _autoReconnect = true; // 재로그인 시 자동 재연결 복원 // 로그인 완료 시 WebSocket 자동 연결 if (!_isConnected && !_isConnecting) { ConnectAsync().Forget(); } }
🧹 Nitpick comments (31)
Assets/Domain/Chat/Model/CharacterActionData.cs (5)
18-19
: 불변성 보호: Actions setter 공개로 불변 조건 깨질 수 있음외부에서 Actions 전체를 교체하면 생성자/파서 보장(최소 1개, 첫 항목=주 액션)이 깨질 수 있습니다. setter를 private으로 제한하세요.
- public List<CharacterActionType> Actions { get; set; } = new List<CharacterActionType>(); + public List<CharacterActionType> Actions { get; private set; } = new List<CharacterActionType>();
50-55
: 공백-only 입력 시 의도치 않은 Idle로 폴백되는 버그actions가 null은 아니지만 빈 문자열/공백만 담을 때 ParseActionsArray가 빈 리스트를 반환하고, FirstOrDefault()로 인해 ActionType이 0(Idle/None)로 설정됩니다. 기본 정책(Talk)과 상충합니다. 파서에서 공백 제외 및 최종 보정, 그리고 여기서는 Actions[0]로 설정하는 편이 안전합니다.
- Emotion = emotion; - Actions = ParseActionsArray(actions); - ActionType = Actions.FirstOrDefault(); + Emotion = emotion; + Actions = ParseActionsArray(actions); + ActionType = Actions[0]; // ParseActionsArray가 최소 1개 보장
80-95
: 문화권 독립 소문자화 사용ToLower()는 문화권 의존적입니다. ToLowerInvariant()로 교체하세요. 터키어 등에서 오인 매핑을 방지합니다.
- return actionString.ToLower() switch + return actionString.ToLowerInvariant() switch
104-113
: 배열 파싱 견고성 향상: 공백/빈 항목 필터링 및 최종 폴백 보장현재 null/Length=0만 기본 Talk로 처리합니다. 공백-only 배열은 빈 리스트가 되어 상위에서 Idle로 귀결됩니다. 공백·빈 문자열 제거, Trim, 최종 폴백을 추가하세요.
- if (actions == null || actions.Length == 0) - { - return new List<CharacterActionType> { CharacterActionType.Talk }; - } - - return actions - .Where(action => !string.IsNullOrEmpty(action)) - .Select(ParseActionString) - .ToList(); + if (actions == null || actions.Length == 0) + { + return new List<CharacterActionType> { CharacterActionType.Talk }; + } + + var list = actions + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.Trim()) + .Select(ParseActionString) + .ToList(); + + return list.Count > 0 ? list : new List<CharacterActionType> { CharacterActionType.Talk };
10-14
: 기본값 정책 통일: CharacterActionDefinitions에서 default를 Talk로 변경
- Assets/Domain/Character/Script/CharacterActionDefinitions.cs 73행:
_ => CharacterActionType.Idle
을_ => CharacterActionType.Talk
으로 수정public static CharacterActionType ToCharacterActionType(this CharacterAction action) { return action switch { CharacterAction.Idle => CharacterActionType.Idle, CharacterAction.Talk => CharacterActionType.Talk, CharacterAction.Listen => CharacterActionType.Listen, - _ => CharacterActionType.Idle + _ => CharacterActionType.Talk }; }Assets/Domain/Character/Script/Config/Live2DModelConfig.cs (4)
71-86
: 파일명 파싱 로직 국제화·견고성 개선(소문자화, Split 안정성)ToLowerInvariant 사용 및 Split 시 빈 토큰 제거로 파일명 선두 구분자 등의 엣지 케이스를 방지하세요.
- string fileName = animationClip.name; + string fileName = animationClip.name; - if (fileName.Contains("-")) { - motionGroup = fileName.Split('-')[0].ToLower(); + if (fileName.Contains("-")) { + motionGroup = fileName.Split(new[]{'-'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); } - else if (fileName.Contains("_")) { - motionGroup = fileName.Split('_')[0].ToLower(); + else if (fileName.Contains("_")) { + motionGroup = fileName.Split(new[]{'_'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); } else { - motionGroup = fileName.ToLower(); + motionGroup = fileName.ToLowerInvariant(); }
47-66
: Id/MotionGroup getter 부작용 최소화Id/MotionGroup의 getter에서 내부 상태를 변경하면 런타임 접근만으로 데이터가 변합니다. OnValidate에서만 갱신하고, getter는 순수 접근자 형태가 안전합니다.
- public string Id { - get { - if (animationClip != null && string.IsNullOrEmpty(id)) { - id = animationClip.name; - UpdateMotionGroupFromFileName(); - } - return id; - } - } + public string Id => string.IsNullOrEmpty(id) && animationClip != null ? animationClip.name : id; - public string MotionGroup { - get { - if (animationClip != null && string.IsNullOrEmpty(motionGroup)) { - UpdateMotionGroupFromFileName(); - } - return motionGroup; - } - set => motionGroup = value; - } + public string MotionGroup + { + get => motionGroup; + set => motionGroup = value; + }
124-132
: Animator 모드 사용 시 누락 설정 검증ActionControllerType이 Animator인데 AnimatorController가 null이면 런타임 오류 가능. OnValidate에서 조건부 경고/보정 로직 추가를 권장합니다.
원하면 OnValidate 보강 코드 제안 가능합니다.
143-149
: Auto Idle 간격 범위 재검토(UX 관점)Range(2–30s), 기본 5s는 다소 잦을 수 있습니다. 캐릭터 성격/씬 밀도에 따라 8–12s 기본값도 고려해보세요.
Assets/Tests/WebSocketApiTest.cs (4)
14-18
: 런타임 Start()에서 테스트 실행은 지양하고 PlayMode 테스트로 이전 권장빌드에 포함되면 의도치 않은 로그/오버헤드가 발생할 수 있습니다. Unity Test Framework(PlayMode)로 옮기거나 최소한 에디터/개발 빌드 가드를 두세요.
아래처럼 본문만 가드하면 영향 최소화됩니다:
void Start() { - TestNewApiMessageParsing(); - TestMultipleActionsAndEmotion(); + #if UNITY_EDITOR || DEVELOPMENT_BUILD + TestNewApiMessageParsing(); + TestMultipleActionsAndEmotion(); + #endif }
46-55
: 파싱 성공 후 기본 불변식(타입/메시지타입/필수 필드) 검증 추가 권장런타임 로그만으로는 실패를 조기에 발견하기 어렵습니다. 최소한 타입/필수 필드를 Assert로 보강하세요.
예시:
var response = JsonConvert.DeserializeObject<WebSocketResponse>(newApiMessage); if (response?.Data != null) { + Debug.Assert(response.Type == "chat", "type != chat"); + Debug.Assert(string.Equals(response.MessageType, "json", StringComparison.OrdinalIgnoreCase), "message_type != json"); + Debug.Assert(!string.IsNullOrEmpty(response.Data.Text), "data.text is empty"); + Debug.Assert(!string.IsNullOrEmpty(response.Data.RequestId), "data.request_id is empty");
52-52
: 빈 배열 출력 시 Array.Empty() 사용할당을 줄이고 표현을 간결히 합니다.
- Debug.Log($"[WebSocketApiTest] Actions: [{string.Join(", ", response.Data.Actions ?? new string[0])}]"); + Debug.Log($"[WebSocketApiTest] Actions: [{string.Join(", ", response.Data.Actions ?? Array.Empty<string>())}]");
56-63
: GUID/다중 액션/감정 데이터에 대한 간단 검증 추가실제 기대 동작을 빠르게 깨달을 수 있도록 Debug.Assert를 추가하세요.
var chatMessage = ChatMessage.FromChatData(response.Data); Debug.Log($"[WebSocketApiTest] ✓ ChatMessage 변환 성공"); Debug.Log($"[WebSocketApiTest] ChatMessage.Emotion: {chatMessage.Emotion}"); Debug.Log($"[WebSocketApiTest] ChatMessage.RequestId: {chatMessage.RequestId}"); Debug.Log($"[WebSocketApiTest] ChatMessage.HasMultipleActions: {chatMessage.HasMultipleActions()}"); Debug.Log($"[WebSocketApiTest] ChatMessage.HasEmotionData: {chatMessage.HasEmotionData()}"); +Debug.Assert(Guid.TryParse(chatMessage.RequestId, out _), "RequestId is not a valid GUID"); +Debug.Assert(chatMessage.HasMultipleActions(), "Expected multiple actions"); +Debug.Assert(chatMessage.HasEmotionData(), "Expected emotion data");Assets/Domain/Character/Script/CharacterActionDefinitions.cs (3)
24-33
: 알 수 없는 값 처리: 기본값 대신 예외로 조기 실패 고려새 액션이 추가돼도 침묵 하강(Downgrade)하지 않도록 기본값 반환 대신 예외를 권장합니다.
return action switch { - CharacterAction.Idle => "idle", - CharacterAction.Talk => "talk", - CharacterAction.Listen => "listen", - _ => "idle" + CharacterAction.Idle => "idle", + CharacterAction.Talk => "talk", + CharacterAction.Listen => "listen", + _ => throw new ArgumentOutOfRangeException(nameof(action), action, "Unknown CharacterAction") };
38-47
: Trigger 매핑도 동일하게 엄격 모드 권장기본 트리거 반환 대신 예외로 미스매핑을 조기에 드러내세요.
return action switch { - CharacterAction.Idle => "PlayIdle", - CharacterAction.Talk => "PlayTalk", - CharacterAction.Listen => "PlayListen", - _ => "PlayIdle" + CharacterAction.Idle => "PlayIdle", + CharacterAction.Talk => "PlayTalk", + CharacterAction.Listen => "PlayListen", + _ => throw new ArgumentOutOfRangeException(nameof(action), action, "Unknown CharacterAction") };
66-75
: 테스트 커버리지 제안: 브릿지 대칭성 보증신규/레거시 상호 변환의 대칭성(원복 가능)을 테스트로 고정하세요.
예: 각 enum 값에 대해
value == value.ToCharacterAction().ToCharacterActionType()
를 검증.Assets/Domain/Character/Script/CharacterModelLoader.cs (4)
60-79
: Animator 제거 정책: 성능 위해 컴포넌트 비활성/제거 옵션 고려Live2D 모드에서 Controller 참조만 null로 두면 Animator 업데이트 오버헤드가 남을 수 있습니다. 필요 시 Animator 자체를 비활성하거나 제거하는 옵션을 추가하세요.
if (animator != null && animator.runtimeAnimatorController != null) { Debug.LogWarning($"[CharacterModelLoader] Animator Controller가 설정되어 있습니다. Live2D 호환성을 위해 제거합니다: {modelInstance.name}"); - animator.runtimeAnimatorController = null; - Debug.Log($"[CharacterModelLoader] Animator Controller 제거 완료: {modelInstance.name}"); + animator.runtimeAnimatorController = null; + animator.enabled = false; // 선택: 성능 최적화 + // Destroy(animator); // 선택: 완전 제거(Inspector에서 되살릴 필요 없을 때) + Debug.Log($"[CharacterModelLoader] Animator 비활성/정리 완료: {modelInstance.name}"); }
96-112
: null 반환 시그니처와 불일치: 호출부 NRE 리스크에러 시
null
을 반환하지만 반환형이UniTask<GameObject>
입니다. 호출부가 널체크를 강제받지 못합니다.GameObject?
로 시그니처를 바꾸거나 예외를 던지도록 통일하세요.예(옵션 1, nullable 반환):
-public async UniTask<GameObject> LoadAndInitializeModelAsync(string characterId, Transform parent = null) +public async UniTask<GameObject?> LoadAndInitializeModelAsync(string characterId, Transform parent = null) -private async UniTask<GameObject> CreateModelInstanceAsync(Live2DModelConfig config, string characterId, Transform parent = null) +private async UniTask<GameObject?> CreateModelInstanceAsync(Live2DModelConfig config, string characterId, Transform parent = null)또는(옵션 2) 예외 throw로 계약 강화.
140-147
: 로그 메시지 오타: 추가된 컴포넌트명이 다릅니다
CubismMouthController
추가 시 로그가CubismAudioMouthInput
으로 출력됩니다. 디버깅 혼선을 줄이기 위해 교정하세요.- mouthController = modelInstance.AddComponent<CubismMouthController>(); - Debug.Log($"[CharacterModelLoader] CubismAudioMouthInput 컴포넌트를 추가했습니다: {modelInstance.name}"); + mouthController = modelInstance.AddComponent<CubismMouthController>(); + Debug.Log($"[CharacterModelLoader] CubismMouthController 컴포넌트를 추가했습니다: {modelInstance.name}");
250-275
: 중복 Config 조회 제거: 인자로 전달해 단일 소스로 유지이미
SetupModelComponents
에config
가 있으므로 재조회 대신 전달하세요. 성능/일관성 향상.-private void SetupActionController(GameObject modelInstance) +private void SetupActionController(GameObject modelInstance, Live2DModelConfig configFromSetup) // 호출부 -SetupActionController(modelInstance); +SetupActionController(modelInstance, config); // 메서드 내부 - string modelId = modelInstance.name; - if (_modelRegistry == null || !_modelRegistry.TryGetConfig(modelId, out var config)) + var config = configFromSetup; + if (config == null) { Debug.LogWarning($"[CharacterModelLoader] 모델 '{modelInstance.name}'의 Config를 찾을 수 없습니다. Live2D 방식으로 기본 설정합니다."); SetupLive2DActionController(modelInstance, null); return; }Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
18-19
: 엔드포인트 버전 prefix 하드코딩 제거 제안.버전 문자열(
api/v1
)을 상수/설정으로 분리하면 차후 업그레이드 시 안전합니다.private const string CHAT_ENDPOINT = "chat"; +private const string API_PREFIX = "api/v1"; ... - return await _httpClient.PostAsync<WebSocketResponse>($"api/v1/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken); + return await _httpClient.PostAsync<WebSocketResponse>($"{API_PREFIX}/{CHAT_ENDPOINT}", request, requiresAuth: true, cancellationToken: cancellationToken);(이미 NetworkConfig가 있다면 그 상수를 사용하도록 변경하는 것도 권장합니다.)
Also applies to: 37-37
Assets/Domain/Chat/Model/ChatMessage.cs (1)
39-44
: 타임스탬프 파싱의 문화권/타임존 안전성 개선.
DateTime.TryParse
는 문화권 영향과 Kind 미설정 문제가 있어 서버 ISO-8601 문자열에 취약합니다.DateTimeOffset
+ UTC로 보정 권장.-// 타임스탬프 파싱 (새 API는 string 형태) -if (!string.IsNullOrEmpty(chatData.Timestamp)) -{ - if (DateTime.TryParse(chatData.Timestamp, out var parsedTimestamp)) - { - chatMessage.Timestamp = parsedTimestamp; - } -} +// 타임스탬프 파싱 (ISO-8601 가정, UTC 보정) +if (!string.IsNullOrEmpty(chatData.Timestamp) + && DateTimeOffset.TryParse(chatData.Timestamp, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, + out var dto)) +{ + chatMessage.Timestamp = dto.UtcDateTime; +}Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (3)
2-7
: 불필요한 using 제거로 의존성/컴파일 부담 축소.인터페이스 파일에서 Live2D/Unity 관련 using이 사용되지 않습니다. 제거 권장.
-using Live2D.Cubism.Core; -using Live2D.Cubism.Framework.Motion; -using ProjectVG.Domain.Character.Live2D.Model; -using System.Collections.Generic; -using UnityEngine;
65-69
: 이벤트 노출 형태 재고 제안.델리게이트 ‘속성’은 외부에서 전체 델리게이트를 교체할 수 있어 오용 여지가 큽니다.
event
로 노출하면 구독/해제만 허용되어 안전합니다. 구현체 변경 범위가 커서 이번 PR에서는 선택 사항으로 제안합니다.
37-37
: API 일관성 제안: 메서드 대신 읽기 전용 프로퍼티.
IsPlaying()
을bool IsPlaying { get; }
프로퍼티로 노출하면 상태 성격에 더 부합합니다. 구현 변경 범위 고려해 선택 사항으로 남깁니다.Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs (1)
332-339
: ‘재연결을 시도합니다’ 로그 조건부 출력 제안_autoReconnect=false일 때도 메시지 문구가 재연결 시도로 읽힙니다. 조건에 따라 문구를 분기하면 혼동 감소.
- Debug.LogWarning("[WebSocket] 연결이 끊어졌습니다. 재연결을 시도합니다."); + Debug.LogWarning(_autoReconnect + ? "[WebSocket] 연결이 끊어졌습니다. 재연결을 시도합니다." + : "[WebSocket] 연결이 끊어졌습니다. 자동 재연결 비활성화 상태입니다.");Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (1)
124-151
: Animator Trigger/State 문자열 사용 → 해시 사용으로 GC/성능 개선StringToHash 캐시를 도입하면 트리거/상태 비교 성능과 GC 압박을 줄일 수 있습니다. 액션→트리거 해시 테이블을 미리 구성해 사용하세요.
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (4)
154-157
: 이벤트 핸들러 중복 등록 가능성 — Initialize 다중 호출 대비Initialize가 여러 번 호출되면 AnimationEndHandler가 중복 등록될 수 있습니다. 기존 핸들러 제거 후 다시 등록 or 가드 필요.
- if (_motionController != null) - { - _motionController.AnimationEndHandler += OnLive2DMotionEnd; - } + if (_motionController != null) + { + _motionController.AnimationEndHandler -= OnLive2DMotionEnd; // 중복 방지 + _motionController.AnimationEndHandler += OnLive2DMotionEnd; + }
736-755
: 전환 속도 프리셋이 누적 곱셈으로 왜곡됨SetFast/SetSmooth/SetSlow가 현재 값에 곱해 누적 왜곡됩니다. 기준(base) 값을 별도 보관해 프리셋 적용 시 절대값으로 설정하세요.
992-995
: 프레임마다 Debug.Log 스팸 — 성능/로그 노이즈 우려전환 루프에서 매프레임 로그는 과합니다. 디버그 빌드/Verbose 플래그 하에서만 출력하거나 샘플링(예: 0.2s 간격)하세요.
Also applies to: 1003-1004
438-439
: 오타 수정: ‘즐시’ → ‘즉시’사용자/QA 로그 혼동 방지를 위해 오타를 고쳐주세요.
- Debug.Log("[Live2DCharacterActionController] Talk 모드 즐시 중단 및 Idle 복귀"); + Debug.Log("[Live2DCharacterActionController] Talk 모드 즉시 중단 및 Idle 복귀");
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (24)
Assets/Domain/Character/Script/CharacterActionController.cs
(0 hunks)Assets/Domain/Character/Script/CharacterActionController.cs.meta
(0 hunks)Assets/Domain/Character/Script/CharacterActionDefinitions.cs
(1 hunks)Assets/Domain/Character/Script/CharacterActionDefinitions.cs.meta
(1 hunks)Assets/Domain/Character/Script/CharacterActionType.cs
(1 hunks)Assets/Domain/Character/Script/CharacterActionType.cs.meta
(1 hunks)Assets/Domain/Character/Script/CharacterManager.cs
(6 hunks)Assets/Domain/Character/Script/CharacterModelLoader.cs
(5 hunks)Assets/Domain/Character/Script/Config/Live2DModelConfig.cs
(7 hunks)Assets/Domain/Character/Script/Implementation.meta
(1 hunks)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs
(1 hunks)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs.meta
(1 hunks)Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs
(1 hunks)Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs.meta
(1 hunks)Assets/Domain/Character/Script/Interface.meta
(1 hunks)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs
(1 hunks)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs.meta
(1 hunks)Assets/Domain/Chat/Model/CharacterActionData.cs
(2 hunks)Assets/Domain/Chat/Model/ChatMessage.cs
(2 hunks)Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs
(1 hunks)Assets/Infrastructure/Network/Services/ChatApiService.cs
(2 hunks)Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs
(12 hunks)Assets/Tests/WebSocketApiTest.cs
(1 hunks)Assets/Tests/WebSocketApiTest.cs.meta
(1 hunks)
💤 Files with no reviewable changes (2)
- Assets/Domain/Character/Script/CharacterActionController.cs.meta
- Assets/Domain/Character/Script/CharacterActionController.cs
✅ Files skipped from review due to trivial changes (8)
- Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs.meta
- Assets/Domain/Character/Script/CharacterActionDefinitions.cs.meta
- Assets/Domain/Character/Script/Interface.meta
- Assets/Domain/Character/Script/Interface/ICharacterActionController.cs.meta
- Assets/Domain/Character/Script/CharacterActionType.cs.meta
- Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs.meta
- Assets/Tests/WebSocketApiTest.cs.meta
- Assets/Domain/Character/Script/Implementation.meta
🚧 Files skipped from review as they are similar to previous changes (1)
- Assets/Domain/Character/Script/CharacterManager.cs
🧰 Additional context used
🧬 Code graph analysis (13)
Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (3)
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (20)
CharacterActionType
(376-379)SetupDefaultCallbacks
(245-277)OnEnable
(205-212)IEnumerator
(217-229)IEnumerator
(500-517)IEnumerator
(610-621)IEnumerator
(626-633)IEnumerator
(868-907)IEnumerator
(912-944)IEnumerator
(949-981)Initialize
(145-186)Initialize
(191-200)PlayAction
(283-322)StopCurrentAction
(327-339)ForceStopAndReturnToIdle
(344-360)IsPlaying
(367-370)SetMotionEndBehavior
(638-643)StopCurrentMotion
(648-651)LoopCurrentMotion
(656-659)ReturnToIdle
(664-667)Assets/Domain/Character/Script/CharacterActionDefinitions.cs (4)
CharacterActionType
(66-75)ToTriggerName
(38-47)CharacterAction
(52-61)ToStateName
(24-33)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (10)
CharacterActionType
(43-43)Initialize
(15-15)PlayAction
(21-21)StopCurrentAction
(26-26)ForceStopAndReturnToIdle
(31-31)IsPlaying
(37-37)SetMotionEndBehavior
(48-48)StopCurrentMotion
(53-53)LoopCurrentMotion
(58-58)ReturnToIdle
(63-63)
Assets/Domain/Character/Script/CharacterActionType.cs (5)
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (1)
CharacterActionType
(376-379)Assets/Domain/Character/Script/CharacterActionDefinitions.cs (1)
CharacterActionType
(66-75)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (1)
CharacterActionType
(200-203)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (1)
CharacterActionType
(43-43)Assets/Domain/Chat/Model/CharacterActionData.cs (1)
CharacterActionType
(80-95)
Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (3)
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (11)
Initialize
(145-186)Initialize
(191-200)PlayAction
(283-322)CharacterActionType
(376-379)StopCurrentAction
(327-339)ForceStopAndReturnToIdle
(344-360)IsPlaying
(367-370)SetMotionEndBehavior
(638-643)StopCurrentMotion
(648-651)LoopCurrentMotion
(656-659)ReturnToIdle
(664-667)Assets/Domain/Character/Script/CharacterManager.cs (3)
Initialize
(34-56)PlayAction
(126-132)StopCurrentAction
(137-140)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (11)
Initialize
(66-85)Initialize
(90-104)PlayAction
(110-151)CharacterActionType
(200-203)StopCurrentAction
(156-166)ForceStopAndReturnToIdle
(171-185)IsPlaying
(191-194)SetMotionEndBehavior
(208-213)StopCurrentMotion
(218-221)LoopCurrentMotion
(226-229)ReturnToIdle
(234-237)
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (2)
Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (15)
CharacterActionType
(200-203)Initialize
(66-85)Initialize
(90-104)SetupDefaultCallbacks
(246-262)OnEnable
(34-41)IEnumerator
(46-56)IEnumerator
(268-286)PlayAction
(110-151)StopCurrentAction
(156-166)ForceStopAndReturnToIdle
(171-185)IsPlaying
(191-194)SetMotionEndBehavior
(208-213)StopCurrentMotion
(218-221)LoopCurrentMotion
(226-229)ReturnToIdle
(234-237)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (10)
CharacterActionType
(43-43)Initialize
(15-15)PlayAction
(21-21)StopCurrentAction
(26-26)ForceStopAndReturnToIdle
(31-31)IsPlaying
(37-37)SetMotionEndBehavior
(48-48)StopCurrentMotion
(53-53)LoopCurrentMotion
(58-58)ReturnToIdle
(63-63)
Assets/Tests/WebSocketApiTest.cs (2)
Assets/Domain/Chat/Model/ChatMessage.cs (3)
ChatMessage
(28-54)HasMultipleActions
(74-74)HasEmotionData
(68-68)Assets/Domain/Chat/Model/CharacterActionData.cs (6)
HasMultipleActions
(67-67)CharacterActionData
(8-123)CharacterActionData
(29-33)CharacterActionData
(39-43)CharacterActionData
(50-55)HasEmotion
(73-73)
Assets/Domain/Character/Script/CharacterActionDefinitions.cs (4)
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (1)
CharacterActionType
(376-379)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (1)
CharacterActionType
(200-203)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (1)
CharacterActionType
(43-43)Assets/Domain/Chat/Model/CharacterActionData.cs (1)
CharacterActionType
(80-95)
Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
Assets/Infrastructure/Network/Http/HttpApiClient.cs (12)
UniTask
(100-107)UniTask
(112-119)UniTask
(124-132)UniTask
(134-142)UniTask
(144-152)UniTask
(154-161)UniTask
(163-173)UniTask
(175-182)UniTask
(184-192)UniTask
(265-279)UniTask
(281-299)CancellationToken
(323-338)
Assets/Domain/Chat/Model/ChatMessage.cs (2)
Assets/Domain/Chat/Model/CharacterActionData.cs (5)
CharacterActionData
(8-123)CharacterActionData
(29-33)CharacterActionData
(39-43)CharacterActionData
(50-55)HasMultipleActions
(67-67)Assets/Domain/Chat/Model/VoiceData.cs (2)
VoiceData
(13-18)VoiceData
(20-40)
Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs (8)
Assets/Infrastructure/Auth/TokenManager.cs (5)
TokenManager
(11-354)GetAccessToken
(127-140)GetRefreshToken
(142-145)IsRefreshTokenExpired
(152-155)TokenSet
(110-125)Assets/Infrastructure/Auth/TokenRefreshService.cs (1)
TokenRefreshService
(18-229)Assets/Infrastructure/Network/WebSocket/INativeWebSocket.cs (1)
DispatchMessageQueue
(30-30)Assets/Infrastructure/Network/WebSocket/Platforms/DesktopWebSocket.cs (1)
DispatchMessageQueue
(163-166)Assets/Infrastructure/Network/WebSocket/Platforms/MobileWebSocket.cs (2)
DispatchMessageQueue
(161-164)UniTask
(35-74)Assets/Infrastructure/Network/WebSocket/Platforms/WebGLWebSocket.cs (1)
DispatchMessageQueue
(175-183)Assets/Infrastructure/Network/Configs/NetworkConfig.cs (2)
NetworkConfig
(235-268)GetWebSocketUrl
(160-164)Assets/Domain/Chat/Model/ChatMessage.cs (1)
ChatMessage
(28-54)
Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs (2)
Assets/Domain/Chat/Model/ChatMessage.cs (1)
Serializable
(8-84)Assets/Domain/Chat/Model/VoiceData.cs (1)
Serializable
(6-192)
Assets/Domain/Character/Script/CharacterModelLoader.cs (4)
Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs (1)
TryGetConfig
(18-30)Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (3)
Live2DCharacterActionController
(103-1066)Initialize
(145-186)Initialize
(191-200)Assets/Domain/Character/Script/CharacterManager.cs (1)
Initialize
(34-56)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (3)
Initialize
(66-85)Initialize
(90-104)AnimatorCharacterActionController
(10-311)
Assets/Domain/Character/Script/Config/Live2DModelConfig.cs (5)
Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs (1)
CreateAssetMenu
(6-31)Assets/Domain/Character/Script/CharacterModelLoader.cs (1)
Live2DModelConfig
(84-91)Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs (1)
ReturnToIdle
(664-667)Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (1)
ReturnToIdle
(234-237)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (1)
ReturnToIdle
(63-63)
Assets/Domain/Chat/Model/CharacterActionData.cs (3)
Assets/Domain/Character/Script/CharacterActionDefinitions.cs (1)
CharacterActionType
(66-75)Assets/Domain/Character/Script/Interface/ICharacterActionController.cs (1)
CharacterActionType
(43-43)Assets/Domain/Chat/Model/ChatMessage.cs (1)
HasMultipleActions
(74-74)
🔇 Additional comments (15)
Assets/Domain/Character/Script/Config/Live2DModelConfig.cs (3)
133-140
: Fade 리스트/모션 리스트 null 처리 가이드Live2D 모드에서 MotionClips 비어있거나 FadeMotionList가 null인 경우 컨트롤러가 널 가드/폴백을 갖는지 확인 필요.
검증 포인트: Live2DCharacterActionController 초기화/재생 경로의 null 체크.
260-272
: OnValidate 내 하위 클립 검증 호출은 좋습니다motionClips를 순회해 하위 OnValidate를 호출하는 구조가 Unity의 OnValidate 제한을 우회하는 실용적 접근입니다. LGTM.
31-41
: ReadOnlyAttribute 정의 확인됨
프로젝트에Assets/Core/Attribute/ReadOnlyAttribute.cs
에public class ReadOnlyAttribute : PropertyAttribute {}
정의가 있어 컴파일 오류는 발생하지 않습니다.Assets/Tests/WebSocketApiTest.cs (2)
80-96
: LGTM — 액션/감정 케이스 커버리지 유용다양한 액션 조합과 감정 플래그를 빠르게 검증하는데 도움이 됩니다.
28-43
: 검증 완료: WebSocketResponse 및 ChatData의 JsonProperty가 테스트 JSON의 모든 키(type
,message_type
,data.text
,data.emotion
,data.actions
,data.order
,data.request_id
,data.timestamp
,data.audio_data
,data.audio_format
,data.audio_length
)와 일치합니다.
모든 매핑이 올바르므로 추가 조치 불필요합니다.Assets/Domain/Character/Script/CharacterActionDefinitions.cs (2)
9-14
: 새 CharacterAction enum 도입 적절Idle/Talk/Listen의 코어 셋으로 시작하는 결정 합리적입니다.
52-61
: 양방향 브릿지(Old↔New) 매핑 일관성 양호레거시
CharacterActionType
과 신규CharacterAction
간 변환 규칙이 대칭으로 잘 정의되어 있습니다.Assets/Domain/Character/Script/CharacterModelLoader.cs (4)
119-127
: 초기화 순서 적절Animator 클리어 → LipSync/입력 → Motion/Expression/Fade → 액션 컨트롤러 순으로 의존성이 잘 정리됐습니다.
226-249
: Fade 컨트롤러 설정 및 가이드 로그 좋습니다
CubismFadeMotionList
미설정 시 구체적 해결 가이드를 제공하는 점이 유용합니다.Refresh()
호출 순서도 적절합니다.
280-303
: Live2D 액션 컨트롤러 초기화 경로 합리적모션 컨트롤러 의존성 체크와 빈 리스트 폴백이 안전합니다.
309-334
: Animator 경로 초기화 흐름 적절Animator 존재/Controller 유효성 검사 후 초기화 순서가 명확합니다.
Assets/Infrastructure/Network/Services/ChatApiService.cs (1)
32-33
: 호출부 및 반환형 변경 검증 완료
SendChatAsync(string, string)
오버로드 호출 잔존 없음- 코드 내
ChatResponse
참조 없음- 모든
WebSocketResponse
소비처에서?.Data
null guard 적용됨Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs (2)
9-19
: 새 Payload 구조 정리 잘 되었습니다.
WebSocketResponse
+ChatData
로 구조화하고 불필요한 레거시 필드를 제거한 점 좋습니다. 도메인 매핑과도 일치합니다.Also applies to: 21-50
17-19
: 해결:response.Data
null 방어 로직이 WebSocketManager와 WebSocketApiTest에서 이미 구현되어 있습니다.Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs (1)
34-41
: 활성화 시 Idle 지연 시작 처리 적절함Animator 초기화 타이밍 대응이 좋아 보입니다.
/// <summary> | ||
/// 대기 상태 | ||
/// </summary> | ||
Idle, | ||
|
||
/// <summary> | ||
/// 듣기 상태 | ||
/// </summary> | ||
Listen, | ||
|
||
/// <summary> | ||
/// 말하기 상태 | ||
/// </summary> | ||
Talk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
기본값 불일치 가능성: enum 기본값(0=Idle) vs 다른 모듈 기본값(Talk) 상충
CharacterActionDefinitions의 기본 매핑은 Idle, CharacterActionData의 기본·파싱 기본은 Talk입니다. 비어있는/알 수 없는 입력에서 액션이 서로 다르게 해석될 수 있습니다. 운영 중 예측 불가 동작을 유발합니다.
권장사항(둘 중 하나):
- enum에 명시적 값과 None을 추가해 기본값 혼선을 차단하고, 레거시 의존성도 명확화
- 혹은 전체 기본값 정책을 Talk로 통일(Definitions의 default 변경)
enum 개선안(명시적 값 부여 및 None 추가):
public enum CharacterActionType
{
+ /// <summary>
+ /// 미정 상태(기본값 보호용)
+ /// </summary>
+ None = 0,
- /// <summary>
- /// 대기 상태
- /// </summary>
- Idle,
+ /// <summary>
+ /// 대기 상태
+ /// </summary>
+ Idle = 1,
- /// <summary>
- /// 듣기 상태
- /// </summary>
- Listen,
+ /// <summary>
+ /// 듣기 상태
+ /// </summary>
+ Listen = 2,
- /// <summary>
- /// 말하기 상태
- /// </summary>
- Talk
+ /// <summary>
+ /// 말하기 상태
+ /// </summary>
+ Talk = 3
}
검증 스크립트(새 기본값 영향 범위 탐지):
🏁 Script executed:
#!/bin/bash
# enum 기본값(0) 의존 코드 탐색
rg -nP --type=cs -C2 '\bCharacterActionType\b' | rg -nP '=\s*0\b|default\s*\(\s*CharacterActionType\s*\)'
# 알 수 없는/기본 분기에서 Idle로 귀결되는 코드 탐색
rg -nP --type=cs -C2 '=>\s*CharacterActionType\.Idle\b|return\s+CharacterActionType\.Idle\b'
Length of output: 1627
🏁 Script executed:
#!/bin/bash
# 전체 파일 상단 200줄 확인
sed -n '1,200p' Assets/Domain/Chat/Model/CharacterActionData.cs
echo -e "\n--- actionString 기반 switch 매핑 ---"
rg -nP --type=cs 'actionString' -C4 Assets/Domain/Chat/Model/CharacterActionData.cs
echo -e "\n--- default/fallback 사례 확인 ---"
rg -nP --type=cs '_\s*=>' -C2 Assets/Domain/Chat/Model/CharacterActionData.cs
Length of output: 5360
기본값 불일치: CharacterActionData(Talk) vs CharacterActionDefinitions(Idle)
CharacterActionData
생성·파싱 실패 시ActionType
이Talk
인 반면,CharacterActionDefinitions
의_ => CharacterActionType.Idle
로Idle
이 기본값으로 사용되어 모듈 간 기본 동작이 상이합니다.- enum에
None = 0
을 추가해 기본값 혼선을 방지하거나,CharacterActionDefinitions
의 기본 fallback을Talk
으로 통일해 일관된 기본 정책을 적용하세요.
(옵션: 아래와 같이 enum을 수정)
public enum CharacterActionType
{
+ /// <summary>
+ /// 미정 상태(기본값 보호용)
+ /// </summary>
+ None = 0,
/// <summary>
/// 대기 상태
/// </summary>
- Idle,
+ Idle = 1,
/// <summary>
/// 듣기 상태
/// </summary>
- Listen,
+ Listen = 2,
/// <summary>
/// 말하기 상태
/// </summary>
- Talk
+ Talk = 3
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
/// <summary> | |
/// 대기 상태 | |
/// </summary> | |
Idle, | |
/// <summary> | |
/// 듣기 상태 | |
/// </summary> | |
Listen, | |
/// <summary> | |
/// 말하기 상태 | |
/// </summary> | |
Talk | |
public enum CharacterActionType | |
{ | |
/// <summary> | |
/// 미정 상태(기본값 보호용) | |
/// </summary> | |
None = 0, | |
/// <summary> | |
/// 대기 상태 | |
/// </summary> | |
Idle = 1, | |
/// <summary> | |
/// 듣기 상태 | |
/// </summary> | |
Listen = 2, | |
/// <summary> | |
/// 말하기 상태 | |
/// </summary> | |
Talk = 3 | |
} |
🤖 Prompt for AI Agents
In Assets/Domain/Character/Script/CharacterActionType.cs around lines 10 to 23,
the enum currently starts with Idle as the first value which causes mismatch
with CharacterActionData defaulting to Talk; add an explicit None = 0 member as
the zero-value default (or alternatively set enum ordering so None is 0) to
avoid implicit defaulting, and update any code that relied on implicit
first-member defaults (or adjust CharacterActionDefinitions fallback to
CharacterActionType.Talk) so both modules use the same default action policy.
/// <summary> | ||
/// Animator Controller를 제거한다 (Live2D 모드에서만, Live2D와 충돌 방지) | ||
/// </summary> | ||
private void ClearAnimatorController(GameObject modelInstance, Live2DModelConfig? config) | ||
{ | ||
// Animator 모드인 경우 Animator Controller를 제거하지 않음 | ||
if (config != null && config.ActionControllerMode == Live2DModelConfig.ActionControllerType.Animator) | ||
{ | ||
Debug.Log($"[CharacterModelLoader] Animator 모드이므로 Animator Controller를 유지합니다: {modelInstance.name}"); | ||
return; | ||
} | ||
|
||
var animator = modelInstance.GetComponent<Animator>(); | ||
if (animator != null && animator.runtimeAnimatorController != null) | ||
{ | ||
Debug.LogWarning($"[CharacterModelLoader] Animator Controller가 설정되어 있습니다. Live2D 호환성을 위해 제거합니다: {modelInstance.name}"); | ||
animator.runtimeAnimatorController = null; | ||
Debug.Log($"[CharacterModelLoader] Animator Controller 제거 완료: {modelInstance.name}"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
참고: nullable 참조 사용 일관성 확인(#nullable enable 여부)
이 파일에서 Live2DModelConfig?
를 사용하지만 파일/프로젝트 레벨에서 #nullable
이 꺼져 있으면 의미가 약해집니다. 프로젝트 전역 또는 파일 상단에서 #nullable enable
을 켜거나 ?
를 제거해 일관성을 맞추세요.
다음 스크립트로 프로젝트의 nullable 설정을 확인하세요:
Also applies to: 96-112, 118-128, 280-303
🏁 Script executed:
#!/bin/bash
# 파일 내 #nullable 및 전역 설정 단서(csc.rsp, Directory.Build.props 등) 조회
rg -n "^#nullable" -S --type=cs
fd -a csc.rsp Directory.Build.props *.csproj | xargs -I{} sh -c 'echo "==> {}"; cat "{}" | rg -n "nullable|langversion|<Nullable>" -n -N || true'
Length of output: 2203
🏁 Script executed:
#!/bin/bash
set -e
echo "=== .csproj 파일 목록 ==="
fd -t f -e csproj
echo
echo "=== Directory.Build.props 파일 검색 ==="
fd -t f -e props -g "Directory.Build.props" || echo "Directory.Build.props 없음"
echo
echo "=== .csproj 내 <Nullable> 설정 ==="
rg -n "<Nullable>" -g "*.csproj" || echo ".csproj에 Nullable 설정 없음"
echo
echo "=== Directory.Build.props 내 Nullable 설정 ==="
rg -n "Nullable" -g "Directory.Build.props" || echo "Directory.Build.props에 Nullable 설정 없음"
Length of output: 917
파일 상단에 #nullable enable
추가 필요
- CharacterModelLoader.cs에 nullable 참조형(
Live2DModelConfig?
)을 올바르게 사용하려면, 파일 최상단에#nullable enable
을 추가하거나 프로젝트의 csproj/Directory.Build.props에<Nullable>enable</Nullable>
설정을 적용해야 합니다. - 동일 파일 내 60–79, 96–112, 118–128, 280–303 구간 모두 마찬가지입니다.
🤖 Prompt for AI Agents
In Assets/Domain/Character/Script/CharacterModelLoader.cs around lines 60 to 79
(and likewise for ranges 96–112, 118–128, 280–303), nullable reference types are
being used (e.g., Live2DModelConfig?) but the file does not enable nullability;
add "#nullable enable" at the very top of this file (or alternatively enable
Nullable in the project via csproj/Directory.Build.props with
<Nullable>enable</Nullable>) so the nullable annotations are properly enforced
by the compiler and avoid nullable-safety warnings; ensure the directive is the
first non-comment line in the file.
private IEnumerator WaitForAnimationAndReturnToIdle(CharacterActionType actionType) | ||
{ | ||
if (_animator == null) yield break; | ||
|
||
var characterAction = actionType.ToCharacterAction(); | ||
string stateName = characterAction.ToStateName(); | ||
|
||
// 애니메이션이 시작될 때까지 대기 | ||
yield return new WaitUntil(() => IsInState(stateName)); | ||
|
||
// 애니메이션이 완료될 때까지 대기 | ||
yield return new WaitUntil(() => !IsInState(stateName) || GetNormalizedTime() >= 0.95f); | ||
|
||
// Idle로 돌아가기 | ||
if (_currentAction == actionType) // 중간에 다른 액션이 실행되지 않았다면 | ||
{ | ||
OnMotionReturnToIdle?.Invoke(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
애니메이션 대기 코루틴 영구 대기 가능성 (타임아웃/폴백 없음)
상태명 불일치 등으로 대상 상태에 진입하지 못하면 코루틴이 영구 대기할 수 있습니다. 타임아웃과 폴백을 추가하세요.
- private IEnumerator WaitForAnimationAndReturnToIdle(CharacterActionType actionType)
+ private IEnumerator WaitForAnimationAndReturnToIdle(CharacterActionType actionType, float maxWaitSeconds = 5f)
{
if (_animator == null) yield break;
var characterAction = actionType.ToCharacterAction();
string stateName = characterAction.ToStateName();
// 애니메이션이 시작될 때까지 대기
- yield return new WaitUntil(() => IsInState(stateName));
+ float start = Time.time;
+ yield return new WaitUntil(() => IsInState(stateName) || Time.time - start > maxWaitSeconds);
+ if (!IsInState(stateName)) yield break; // 시작 실패 시 종료
// 애니메이션이 완료될 때까지 대기
- yield return new WaitUntil(() => !IsInState(stateName) || GetNormalizedTime() >= 0.95f);
+ yield return new WaitUntil(() =>
+ !IsInState(stateName) || GetNormalizedTime() >= 0.95f || Time.time - start > maxWaitSeconds);
// Idle로 돌아가기
if (_currentAction == actionType) // 중간에 다른 액션이 실행되지 않았다면
{
OnMotionReturnToIdle?.Invoke();
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private IEnumerator WaitForAnimationAndReturnToIdle(CharacterActionType actionType) | |
{ | |
if (_animator == null) yield break; | |
var characterAction = actionType.ToCharacterAction(); | |
string stateName = characterAction.ToStateName(); | |
// 애니메이션이 시작될 때까지 대기 | |
yield return new WaitUntil(() => IsInState(stateName)); | |
// 애니메이션이 완료될 때까지 대기 | |
yield return new WaitUntil(() => !IsInState(stateName) || GetNormalizedTime() >= 0.95f); | |
// Idle로 돌아가기 | |
if (_currentAction == actionType) // 중간에 다른 액션이 실행되지 않았다면 | |
{ | |
OnMotionReturnToIdle?.Invoke(); | |
} | |
} | |
private IEnumerator WaitForAnimationAndReturnToIdle(CharacterActionType actionType, float maxWaitSeconds = 5f) | |
{ | |
if (_animator == null) yield break; | |
var characterAction = actionType.ToCharacterAction(); | |
string stateName = characterAction.ToStateName(); | |
// 애니메이션이 시작될 때까지 대기 (타임아웃 포함) | |
float start = Time.time; | |
yield return new WaitUntil(() => IsInState(stateName) || Time.time - start > maxWaitSeconds); | |
if (!IsInState(stateName)) yield break; // 시작 실패 시 종료 | |
// 애니메이션이 완료될 때까지 대기 (타임아웃 포함) | |
yield return new WaitUntil(() => | |
!IsInState(stateName) || GetNormalizedTime() >= 0.95f || Time.time - start > maxWaitSeconds); | |
// Idle로 돌아가기 | |
if (_currentAction == actionType) // 중간에 다른 액션이 실행되지 않았다면 | |
{ | |
OnMotionReturnToIdle?.Invoke(); | |
} | |
} |
🤖 Prompt for AI Agents
In
Assets/Domain/Character/Script/Implementation/AnimatorCharacterActionController.cs
around lines 268 to 286, the WaitForAnimationAndReturnToIdle coroutine can block
forever if the animator never enters the expected state; add a timeout/fallback
so the coroutine always proceeds. Implement a cancellation timeout (e.g., track
elapsed time using Time.time or a stopwatch and a configurable maxWaitSeconds)
and break the WaitUntil loops when the timeout elapses; after timeout, log a
warning, optionally force invoke OnMotionReturnToIdle or set animator to Idle
state to avoid desync, and ensure the coroutine exits cleanly. Make the timeout
configurable via a private const or serialized field and apply it to both waits
so neither can hang indefinitely.
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | ||
{ | ||
yield return new WaitForSeconds(_transitionDuration * 0.1f); // Priority 충돌 방지용 짧은 대기 | ||
|
||
if (_motionController != null) | ||
{ | ||
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | ||
_motionController.PlayAnimation(clip, priority: 2, isLoop: false); | ||
_isPlayingAction = true; | ||
|
||
// 모션 종료 처리 설정 | ||
_currentMotionEndCallback = endCallback; | ||
StopAllMotionCoroutines(); | ||
_currentMotionCoroutine = StartCoroutine(WaitForMotionEnd(clip.length, endCallback)); | ||
|
||
Debug.Log($"[Live2DCharacterActionController] 모션 재생: {_currentMotionClip?.Id} (그룹: {_currentMotionClip?.MotionGroup}), 길이: {clip.length}s"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우선순위 인자 무시 버그 — 전달받은 priority 값이 하드코딩 2로 대체됨
DelayedMotionPlay에서 priority 파라미터를 무시합니다. 실제 인자를 사용해야 합니다.
- _motionController.PlayAnimation(clip, priority: 2, isLoop: false);
+ _motionController.PlayAnimation(clip, priority: priority, isLoop: false);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | |
{ | |
yield return new WaitForSeconds(_transitionDuration * 0.1f); // Priority 충돌 방지용 짧은 대기 | |
if (_motionController != null) | |
{ | |
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | |
_motionController.PlayAnimation(clip, priority: 2, isLoop: false); | |
_isPlayingAction = true; | |
// 모션 종료 처리 설정 | |
_currentMotionEndCallback = endCallback; | |
StopAllMotionCoroutines(); | |
_currentMotionCoroutine = StartCoroutine(WaitForMotionEnd(clip.length, endCallback)); | |
Debug.Log($"[Live2DCharacterActionController] 모션 재생: {_currentMotionClip?.Id} (그룹: {_currentMotionClip?.MotionGroup}), 길이: {clip.length}s"); | |
} | |
private IEnumerator DelayedMotionPlay(AnimationClip clip, int priority, System.Action? endCallback) | |
{ | |
yield return new WaitForSeconds(_transitionDuration * 0.1f); // Priority 충돌 방지용 짧은 대기 | |
if (_motionController != null) | |
{ | |
// 모션 재생 (CubismFadeController가 자동 페이드 처리) | |
_motionController.PlayAnimation(clip, priority: priority, isLoop: false); | |
_isPlayingAction = true; | |
// 모션 종료 처리 설정 | |
_currentMotionEndCallback = endCallback; | |
StopAllMotionCoroutines(); | |
_currentMotionCoroutine = StartCoroutine(WaitForMotionEnd(clip.length, endCallback)); | |
Debug.Log($"[Live2DCharacterActionController] 모션 재생: {_currentMotionClip?.Id} (그룹: {_currentMotionClip?.MotionGroup}), 길이: {clip.length}s"); | |
} | |
} |
🤖 Prompt for AI Agents
In
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs
around lines 500 to 516, the DelayedMotionPlay method ignores the incoming
priority parameter by passing a hardcoded 2 to _motionController.PlayAnimation;
change the call to use the priority variable passed into the method (e.g.,
PlayAnimation(clip, priority: priority, isLoop: false)). Keep the surrounding
logic (null check, setting _isPlayingAction, callbacks, coroutine management,
and debug log) unchanged.
private void StopAllMotionCoroutines() | ||
{ | ||
// 이 컴포넌트가 시작한 모든 코루틴 중지 | ||
StopAllCoroutines(); | ||
_currentMotionCoroutine = null; | ||
_currentMotionEndCallback = null; | ||
_transitionCoroutine = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
코루틴 중지 시 전환 상태(_currentTransition) 초기화 누락 → IsTransitioning()가 영구 true 가능
StopAllMotionCoroutines에서 _currentTransition을 null로 리셋하세요. 그렇지 않으면 Idle 시퀀스 재생이 막힐 수 있습니다.
private void StopAllMotionCoroutines()
{
// 이 컴포넌트가 시작한 모든 코루틴 중지
StopAllCoroutines();
_currentMotionCoroutine = null;
_currentMotionEndCallback = null;
- _transitionCoroutine = null;
+ _transitionCoroutine = null;
+ _currentTransition = null;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private void StopAllMotionCoroutines() | |
{ | |
// 이 컴포넌트가 시작한 모든 코루틴 중지 | |
StopAllCoroutines(); | |
_currentMotionCoroutine = null; | |
_currentMotionEndCallback = null; | |
_transitionCoroutine = null; | |
} | |
private void StopAllMotionCoroutines() | |
{ | |
// 이 컴포넌트가 시작한 모든 코루틴 중지 | |
StopAllCoroutines(); | |
_currentMotionCoroutine = null; | |
_currentMotionEndCallback = null; | |
_transitionCoroutine = null; | |
_currentTransition = null; | |
} |
🤖 Prompt for AI Agents
In
Assets/Domain/Character/Script/Implementation/Live2DCharacterActionController.cs
around lines 795 to 802, StopAllMotionCoroutines currently stops coroutines and
clears motion-related fields but fails to reset _currentTransition; update the
method to also set _currentTransition = null so that IsTransitioning() does not
remain true and idle sequences can resume normally.
public static ChatMessage FromChatData(ChatData chatData) | ||
{ | ||
var chatMessage = new ChatMessage { | ||
SessionId = response.SessionId, | ||
Text = response.Text, | ||
Timestamp = response.Timestamp, | ||
ActionData = new CharacterActionData(response.Action) | ||
Text = chatData.Text, | ||
Emotion = chatData.Emotion, | ||
Order = chatData.Order, | ||
RequestId = chatData.RequestId, | ||
ActionData = new CharacterActionData(chatData.Actions, chatData.Emotion) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NRE 방지: 입력 파라미터 가드 추가.
chatData
가 null이면 즉시 크래시합니다. 명시적 가드 추가를 권장합니다.
public static ChatMessage FromChatData(ChatData chatData)
{
+ if (chatData == null)
+ throw new ArgumentNullException(nameof(chatData));
+
var chatMessage = new ChatMessage {
Text = chatData.Text,
Emotion = chatData.Emotion,
Order = chatData.Order,
RequestId = chatData.RequestId,
ActionData = new CharacterActionData(chatData.Actions, chatData.Emotion)
};
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static ChatMessage FromChatData(ChatData chatData) | |
{ | |
var chatMessage = new ChatMessage { | |
SessionId = response.SessionId, | |
Text = response.Text, | |
Timestamp = response.Timestamp, | |
ActionData = new CharacterActionData(response.Action) | |
Text = chatData.Text, | |
Emotion = chatData.Emotion, | |
Order = chatData.Order, | |
RequestId = chatData.RequestId, | |
ActionData = new CharacterActionData(chatData.Actions, chatData.Emotion) | |
}; | |
public static ChatMessage FromChatData(ChatData chatData) | |
{ | |
if (chatData == null) | |
throw new ArgumentNullException(nameof(chatData)); | |
var chatMessage = new ChatMessage { | |
Text = chatData.Text, | |
Emotion = chatData.Emotion, | |
Order = chatData.Order, | |
RequestId = chatData.RequestId, | |
ActionData = new CharacterActionData(chatData.Actions, chatData.Emotion) | |
}; |
🤖 Prompt for AI Agents
In Assets/Domain/Chat/Model/ChatMessage.cs around lines 28 to 36, the static
factory method FromChatData does not guard against a null chatData and will NRE;
add an explicit null check at the start (e.g. if (chatData == null) throw new
ArgumentNullException(nameof(chatData))); so the method fails fast with a clear
exception instead of crashing later; update any callers if they expect null
returns.
if (!string.IsNullOrEmpty(chatData.AudioData)) | ||
{ | ||
chatMessage.CostInfo = new CostInfo(response.UsedCost ?? 0f, response.RemainingCost ?? 0f); | ||
chatMessage.VoiceData = VoiceData.FromBase64(chatData.AudioData, chatData.AudioFormat); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
오디오 포맷 null 처리 보강.
chatData.AudioFormat
가 null이면 내부 변환 유틸에서 NRE가 발생할 수 있습니다. 기본값 보정이 안전합니다.
-if (!string.IsNullOrEmpty(chatData.AudioData))
-{
- chatMessage.VoiceData = VoiceData.FromBase64(chatData.AudioData, chatData.AudioFormat);
-}
+if (!string.IsNullOrEmpty(chatData.AudioData))
+{
+ var format = string.IsNullOrEmpty(chatData.AudioFormat) ? "wav" : chatData.AudioFormat;
+ chatMessage.VoiceData = VoiceData.FromBase64(chatData.AudioData, format);
+}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (!string.IsNullOrEmpty(chatData.AudioData)) | |
{ | |
chatMessage.CostInfo = new CostInfo(response.UsedCost ?? 0f, response.RemainingCost ?? 0f); | |
chatMessage.VoiceData = VoiceData.FromBase64(chatData.AudioData, chatData.AudioFormat); | |
} | |
if (!string.IsNullOrEmpty(chatData.AudioData)) | |
{ | |
var format = string.IsNullOrEmpty(chatData.AudioFormat) ? "wav" : chatData.AudioFormat; | |
chatMessage.VoiceData = VoiceData.FromBase64(chatData.AudioData, format); | |
} |
🤖 Prompt for AI Agents
In Assets/Domain/Chat/Model/ChatMessage.cs around lines 48 to 51, the code uses
chatData.AudioFormat directly which can be null and cause a
NullReferenceException inside VoiceData.FromBase64; update the assignment to
normalize the format first (e.g., use a safe default like "wav" or a predefined
constant when chatData.AudioFormat is null or empty) and pass the non-null
normalized format into VoiceData.FromBase64 so the converter never receives a
null format.
Summary by CodeRabbit
New Features
Refactor
Platform
Chores