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.
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.
💡 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.
🛠️ 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.
💡 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' -C2Length 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}' -C2Length 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.
🛠️ 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.
토큰 값 전체/부분 로그 노출 제거
액세스/리프레시 토큰 및 만료/디바이스 정보를 그대로 출력합니다. 민감정보 노출 위험이 큽니다. 마스킹 또는 길이만 로그로 남기세요. 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.
🛠️ 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.
🛠️ 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.
💡 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
fiLength 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.
💡 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.
💡 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 || trueLength 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 || trueLength of output: 318
🏁 Script executed:
#!/bin/bash
# autoPublish 설정 확인을 위해 8~16번째 줄 출력
sed -n '8,16p' ProjectSettings/Packages/com.unity.connect.share/Settings.jsonLength 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.
| 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.
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.
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.
🛠️ 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.
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.
민감정보 노출: 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.
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.unityis 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.
💡 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' -SLength 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.csLength 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.
🛠️ 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.
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.
민감 파라미터(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.
🛠️ 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.
🛠️ 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.
🛠️ 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.
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소비처에서?.Datanull guard 적용됨Assets/Infrastructure/Network/DTOs/Chat/ChatResponse.cs (2)
9-19: 새 Payload 구조 정리 잘 되었습니다.
WebSocketResponse+ChatData로 구조화하고 불필요한 레거시 필드를 제거한 점 좋습니다. 도메인 매핑과도 일치합니다.Also applies to: 21-50
17-19: 해결:response.Datanull 방어 로직이 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.
💡 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.csLength 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.
💡 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.
🛠️ 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.
우선순위 인자 무시 버그 — 전달받은 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.
🛠️ 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.
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.
🛠️ 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.
tigran-sargsyan-w
left a comment
There was a problem hiding this comment.
Hey, just passing by as an external dev.
Overall this looks like a pretty big but nice step forward – character motion controllers, Live2D/Animator implementations, auth/JWT flow and WebSocket bits all in one.
A few random thoughts while scrolling through the changes:
- The PR is huge (+9k / -1k across a lot of files). For future iterations it might be easier to review if auth, WebSocket and character-motion changes are split into separate PRs.
- It looks like
Domain/Character,ChatandInfrastructure/Authall evolve together here. If you want to keep the architecture clean long‑term, it could be worth double‑checking that domain code doesn’t start depending too much on infrastructure details. - For the auth / JWT / token refresh flow, some small unit or integration tests (or even a short usage example in the PR description) would help others understand the expected lifecycle and edge cases.
- In the animation controllers, if there are any Animator or Live2D state names used as raw strings, it might be safer to centralize them in enums/consts to avoid typo bugs later.
Nothing blocking from my side, just sharing thoughts as someone reading this from the outside – the direction looks solid 👍
Summary by CodeRabbit
New Features
Refactor
Platform
Chores