Conversation
- Rust: auth.rs (토큰 관리), api_client.rs (서버 통신), commands/auth.rs (OAuth 플로우) - 로그인 시 서버→로컬 데이터 sync, 설정 변경 시 로컬→서버 push - 패널 열 때 + 5분 주기 polling으로 서버 데이터 자동 pull - 온보딩 welcome 화면에 소셜 로그인 버튼 추가 - 설정 화면에 계정 섹션(로그인/로그아웃) 추가 - 비로그인 시 기존 로컬 전용 동작 유지
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 3 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (22)
WalkthroughKakao·Apple 소셜 로그인, 토큰 영구 저장 및 MOA 서버와의 양방향 설정 동기화가 추가되었고, 이를 위한 Rust API 클라이언트·인증 모듈·주기 동기화와 프론트엔드 바인딩·훅·UI 요소가 연동되었습니다. 환경 예시 및 몇몇 의존성도 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Frontend as Browser UI
participant TauriApp as Tauri App
participant OAuth as OAuth Provider (Kakao/Apple)
participant MOA as MOA API
participant Disk as Local Disk
User->>Frontend: 클릭: 소셜 로그인
Frontend->>TauriApp: commands.socialLogin(provider)
TauriApp->>TauriApp: 로컬 콜백 서버 시작
TauriApp->>OAuth: 인증 URL 오픈
User->>OAuth: 권한 부여
OAuth->>TauriApp: 리다이렉트(코드)
TauriApp->>OAuth: 코드 교환 -> id_token
OAuth-->>TauriApp: id_token
TauriApp->>MOA: auth_login(id_token)
MOA-->>TauriApp: access_token
TauriApp->>Disk: save_auth_token(access_token)
TauriApp->>MOA: get_onboarding_status / profile / payroll
TauriApp->>TauriApp: 로컬 설정 병합 또는 서버로 푸시
TauriApp-->>Frontend: LoginResult 반환
sequenceDiagram
participant TauriApp as Local App
participant MOA as MOA API
participant Disk as Local Disk
TauriApp->>MOA: sync_from_server(access_token)
MOA-->>TauriApp: profile, payroll, work_policy
TauriApp->>Disk: 로컬 설정 업데이트
TauriApp-->>TauriApp: emit settings_changed
TauriApp->>MOA: sync_settings_to_server(access_token)
MOA-->>TauriApp: 패치 응답
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
apps/app/src-tauri/src/auth.rs (2)
18-23: 토큰 저장 보안 고려사항.
auth.json에access_token이 평문으로 저장됩니다. macOS Keychain이나 Windows Credential Manager 같은 시스템 보안 저장소 사용을 향후 고려해 볼 수 있습니다. 현재 구현은 기능적으로 동작하지만, 민감한 토큰의 파일 시스템 노출이 잠재적 위험 요소입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/auth.rs` around lines 18 - 23, AuthFile currently stores access_token as plaintext in auth.json; update the persistence to use the OS secure credential store instead of writing access_token to disk: remove or stop using file-based read/write of AuthFile.access_token and switch to a keyring/credential API (e.g., the keyring crate or platform-specific Keychain/Credential Manager calls) for storing and retrieving the token, ensuring all places that construct, read or persist AuthFile or access_token use the secure store API and fall back to in-memory only.
59-62: Mutex 잠금 해제 시 poisoning 처리 고려.
lock().unwrap()은 Mutex가 poisoned 상태일 때 panic을 유발합니다. 데스크톱 앱에서는 드문 경우지만,lock().unwrap_or_else(|e| e.into_inner())를 사용하면 poisoned 상태에서도 복구할 수 있습니다. 동일 패턴이 line 71, 82에도 적용됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/auth.rs` around lines 59 - 62, Replace the panicking Mutex unwraps with poison-aware handling: where the code acquires the AuthStore mutex (the call that currently uses lock().unwrap() before assigning Some(AuthState { access_token: access_token.to_string() })), change to use lock().unwrap_or_else(|e| e.into_inner()) so a poisoned Mutex is recovered; apply the same replacement to the other two occurrences that lock the same store (the places that set or read AuthState) to avoid panics on poisoned mutexes while preserving the inner guard for assignment or reads.apps/app/src-tauri/src/api_client.rs (1)
184-190: HTTP 클라이언트에 타임아웃 설정 권장.
Client::new()는 기본 타임아웃 없이 생성됩니다. 네트워크 지연 시 요청이 무한정 대기할 수 있어 앱 응답성에 영향을 줄 수 있습니다.⏱️ 타임아웃 설정 제안
+use std::time::Duration; + impl ApiClient { pub fn new(base_url: &str) -> Self { Self { base_url: base_url.trim_end_matches('/').to_string(), - http: Client::new(), + http: Client::builder() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| Client::new()), } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/api_client.rs` around lines 184 - 190, The ApiClient::new currently uses Client::new() with no timeout; change it to build a reqwest client with a sensible default timeout (e.g. 15–30s) using Client::builder().timeout(Duration::from_secs(...)).build() and assign that to the http field, adding use std::time::Duration if needed; optionally expose the timeout as a parameter or constant so callers can override it while keeping base_url trimming logic intact.apps/app/src-tauri/.env.example (1)
5-5:APPLE_PRIVATE_KEY멀티라인 처리 문서화 고려.Apple 개인 키는 일반적으로 PEM 형식의 멀티라인 값입니다.
.env파일에서 멀티라인 값을 처리하는 방법(예:\n으로 이스케이프하거나 별도 파일 참조)에 대한 주석이나 README 설명이 있으면 개발자 온보딩에 도움이 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/.env.example` at line 5, The APPLE_PRIVATE_KEY environment variable in .env.example is a PEM-formatted multiline value and needs documentation on how to store/consume it; update the project docs or the .env.example to explain handling options for APPLE_PRIVATE_KEY (e.g., store as escaped newlines using \n, base64-encode the PEM, or reference a separate file via a new variable like APPLE_PRIVATE_KEY_FILE) and show how the runtime should read it (unescape newlines or read the file) so developers know to use APPLE_PRIVATE_KEY or APPLE_PRIVATE_KEY_FILE consistently when loading the key.apps/app/src-tauri/src/tray.rs (1)
244-257:panel-shown이벤트가 macOS 토글 경로에만 한정됩니다.Line 244, Line 257 추가는 좋지만 현재 구현만으로는 비-macOS의 패널 표시 경로(및
show_main_window경로)에서 동기화 트리거가 빠질 수 있습니다. 이벤트 emit을 “패널 표시” 공통 경로로 묶어 두는 편이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/tray.rs` around lines 244 - 257, 현재 `panel-shown` 이벤트가 macOS 전용 토글 경로에만 한정되어 비-macOS의 패널 표시/`show_main_window` 경로에서 동기화 트리거가 누락될 수 있으니, `app_handle.emit("panel-shown", ())`를 공통 표시 코드로 옮기거나(예: 새로운 helper 함수 emit_panel_shown()) `position_menubar_panel`, `show_main_window` 및 `app_handle.get_webview_window("main")`의 표시 분기 모두에서 호출되도록 추가하여 모든 패널/창 표시 경로에서 항상 이벤트가 방출되게 고치세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/app/src-tauri/src/commands/auth.rs`:
- Around line 244-263: The authorize URL construction for AuthProvider::Kakao
and AuthProvider::Apple (in authorize_url) omits an OAuth state parameter which
makes login CSRF-prone; generate a cryptographically random state value, store
it server-side (e.g., in the session or a secure temporary store) before
building authorize_url (include &state={state} when formatting the redirect),
and then validate the returned state in the authentication callback handler (the
callback that currently only checks code around the earlier 184-203 area) by
comparing the incoming state to the stored one and rejecting requests on
mismatch or missing state; use the existing redirect_uri/urlencoded logic and
ensure state generation uses a secure RNG.
- Around line 529-536: load_local_settings currently reads the settings file
with std::fs and serde_json; replace that direct file access with
tauri-plugin-store so the app uses the same store format/rules. In
load_local_settings, open the Store for the path returned by
get_user_settings_path (via tauri_plugin_store::Store or StoreBuilder), call the
store load/restore method, and then retrieve/deserialize the stored UserSettings
(e.g., store.get::<UserSettings>("user_settings") or reading the top-level store
value) returning UserSettings::default() when missing and converting plugin
errors into the same Result<String, ...> error messages you already use. Ensure
you reference load_local_settings, get_user_settings_path, and UserSettings when
making the change.
- Around line 185-190: The code mistakenly uses listener.set_ttl(120) to try to
enforce an accept timeout; set_ttl adjusts IP TTL, not blocking behavior, so
accept() can still block indefinitely. Change the socket to nonblocking via
listener.set_nonblocking(true) and replace the blocking accept() usage with a
polling loop that repeatedly calls listener.accept() handling WouldBlock and
breaks on success or when a wall-clock timeout elapses (or switch to an async
TcpListener from tokio and use tokio::time::timeout around accept()). Update the
code around the listener variable and the accept call to use nonblocking accept
handling (or tokio::net::TcpListener + tokio::time::timeout) instead of relying
on set_ttl.
- Around line 476-477: The direct casts assigning payroll.salary_amount ->
new_amount and profile.payday_day -> new_pay_day can silently truncate/wrap
invalid values; validate ranges and perform fallible conversions (e.g., use
u32::try_from or i64::try_into and u8::try_from after checking expected bounds)
for payroll.salary_amount and payday_day, returning a Result<T, String> on error
instead of unwrapping—replace the direct assignments to new_amount and
new_pay_day with guarded conversions that check payroll.salary_amount is
non‑negative and within u32::MAX and that profile.payday_day is within 1..=31
before calling try_from()/try_into(), and propagate a descriptive error if
conversion fails.
In `@apps/app/src-tauri/src/salary.rs`:
- Around line 89-95: The loop currently spawns a new async task every
SYNC_INTERVAL_SECS without waiting for completion, allowing overlapping runs of
crate::commands::auth::sync_from_server; add an in-flight guard (e.g., a shared
AtomicBool or tokio::sync::Mutex) checked with compare/set before spawning so a
new task is skipped if one is already running, and ensure the guard is cleared
in all exit paths after sync_from_server completes (use a finally/RAII pattern)
so no concurrent file/state writes occur.
In `@apps/app/src/features/settings/screens/settings-screen.tsx`:
- Around line 151-160: Prevent duplicate social-login starts by checking the
socialLogin pending state before calling mutate: in the InfoRow buttons
(component InfoRow) guard onClick with socialLogin.isPending (early return) or
pass a disabled prop when socialLogin.isPending so clicks are ignored while a
request is pending; ensure both kakao and apple handlers use the same check
around socialLogin.mutate to avoid reentrancy.
---
Nitpick comments:
In `@apps/app/src-tauri/.env.example`:
- Line 5: The APPLE_PRIVATE_KEY environment variable in .env.example is a
PEM-formatted multiline value and needs documentation on how to store/consume
it; update the project docs or the .env.example to explain handling options for
APPLE_PRIVATE_KEY (e.g., store as escaped newlines using \n, base64-encode the
PEM, or reference a separate file via a new variable like
APPLE_PRIVATE_KEY_FILE) and show how the runtime should read it (unescape
newlines or read the file) so developers know to use APPLE_PRIVATE_KEY or
APPLE_PRIVATE_KEY_FILE consistently when loading the key.
In `@apps/app/src-tauri/src/api_client.rs`:
- Around line 184-190: The ApiClient::new currently uses Client::new() with no
timeout; change it to build a reqwest client with a sensible default timeout
(e.g. 15–30s) using Client::builder().timeout(Duration::from_secs(...)).build()
and assign that to the http field, adding use std::time::Duration if needed;
optionally expose the timeout as a parameter or constant so callers can override
it while keeping base_url trimming logic intact.
In `@apps/app/src-tauri/src/auth.rs`:
- Around line 18-23: AuthFile currently stores access_token as plaintext in
auth.json; update the persistence to use the OS secure credential store instead
of writing access_token to disk: remove or stop using file-based read/write of
AuthFile.access_token and switch to a keyring/credential API (e.g., the keyring
crate or platform-specific Keychain/Credential Manager calls) for storing and
retrieving the token, ensuring all places that construct, read or persist
AuthFile or access_token use the secure store API and fall back to in-memory
only.
- Around line 59-62: Replace the panicking Mutex unwraps with poison-aware
handling: where the code acquires the AuthStore mutex (the call that currently
uses lock().unwrap() before assigning Some(AuthState { access_token:
access_token.to_string() })), change to use lock().unwrap_or_else(|e|
e.into_inner()) so a poisoned Mutex is recovered; apply the same replacement to
the other two occurrences that lock the same store (the places that set or read
AuthState) to avoid panics on poisoned mutexes while preserving the inner guard
for assignment or reads.
In `@apps/app/src-tauri/src/tray.rs`:
- Around line 244-257: 현재 `panel-shown` 이벤트가 macOS 전용 토글 경로에만 한정되어 비-macOS의 패널
표시/`show_main_window` 경로에서 동기화 트리거가 누락될 수 있으니, `app_handle.emit("panel-shown",
())`를 공통 표시 코드로 옮기거나(예: 새로운 helper 함수 emit_panel_shown())
`position_menubar_panel`, `show_main_window` 및
`app_handle.get_webview_window("main")`의 표시 분기 모두에서 호출되도록 추가하여 모든 패널/창 표시 경로에서
항상 이벤트가 방출되게 고치세요.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: aeba977b-23b5-447c-b323-6a293bd87d43
⛔ Files ignored due to path filters (1)
apps/app/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
apps/app/src-tauri/.env.exampleapps/app/src-tauri/Cargo.tomlapps/app/src-tauri/src/api_client.rsapps/app/src-tauri/src/auth.rsapps/app/src-tauri/src/bindings.rsapps/app/src-tauri/src/commands/auth.rsapps/app/src-tauri/src/commands/mod.rsapps/app/src-tauri/src/lib.rsapps/app/src-tauri/src/salary.rsapps/app/src-tauri/src/tray.rsapps/app/src/features/onboarding/hooks/use-onboarding-form.tsapps/app/src/features/onboarding/screens/welcome-screen.tsxapps/app/src/features/root-layout.tsxapps/app/src/features/settings/hooks/use-settings-form.tsapps/app/src/features/settings/screens/settings-screen.tsxapps/app/src/hooks/use-auth.tsapps/app/src/lib/tauri-bindings.gen.tsapps/app/src/lib/tauri-bindings.tsapps/app/src/queries/auth.tsapps/app/src/queries/index.ts
| let authorize_url = match &provider { | ||
| AuthProvider::Kakao => { | ||
| let client_id = std::env::var("KAKAO_REST_API_KEY") | ||
| .map_err(|_| "KAKAO_REST_API_KEY 환경변수 미설정")?; | ||
| format!( | ||
| "https://kauth.kakao.com/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid", | ||
| client_id, | ||
| urlencoded(&redirect_uri), | ||
| ) | ||
| } | ||
| AuthProvider::Apple => { | ||
| let client_id = | ||
| std::env::var("APPLE_CLIENT_ID").map_err(|_| "APPLE_CLIENT_ID 환경변수 미설정")?; | ||
| format!( | ||
| "https://appleid.apple.com/auth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid&response_mode=query", | ||
| client_id, | ||
| urlencoded(&redirect_uri), | ||
| ) | ||
| } | ||
| }; |
There was a problem hiding this comment.
OAuth state 검증이 없어 로그인 CSRF에 취약합니다.
인가 요청에 state를 넣지 않고, 콜백에서도 code만 확인하고 있어 외부 주입된 코드로 세션이 바뀔 수 있습니다.
🔐 제안 수정
+// authorize URL 생성 전에 state 생성
+let oauth_state = generate_oauth_state();
// provider별 authorize URL 생성
let authorize_url = match &provider {
AuthProvider::Kakao => {
let client_id = std::env::var("KAKAO_REST_API_KEY")
.map_err(|_| "KAKAO_REST_API_KEY 환경변수 미설정")?;
format!(
- "https://kauth.kakao.com/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid",
+ "https://kauth.kakao.com/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid&state={}",
client_id,
urlencoded(&redirect_uri),
+ urlencoded(&oauth_state),
)
}
AuthProvider::Apple => {
let client_id =
std::env::var("APPLE_CLIENT_ID").map_err(|_| "APPLE_CLIENT_ID 환경변수 미설정")?;
format!(
- "https://appleid.apple.com/auth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid&response_mode=query",
+ "https://appleid.apple.com/auth/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid&response_mode=query&state={}",
client_id,
urlencoded(&redirect_uri),
+ urlencoded(&oauth_state),
)
}
};
-let code = tauri::async_runtime::spawn_blocking(move || wait_for_auth_code(&listener))
+let code = tauri::async_runtime::spawn_blocking(move || wait_for_auth_code(&listener, &oauth_state))
.await
.map_err(|e| format!("콜백 대기 실패: {e}"))??;Also applies to: 184-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/app/src-tauri/src/commands/auth.rs` around lines 244 - 263, The
authorize URL construction for AuthProvider::Kakao and AuthProvider::Apple (in
authorize_url) omits an OAuth state parameter which makes login CSRF-prone;
generate a cryptographically random state value, store it server-side (e.g., in
the session or a secure temporary store) before building authorize_url (include
&state={state} when formatting the redirect), and then validate the returned
state in the authentication callback handler (the callback that currently only
checks code around the earlier 184-203 area) by comparing the incoming state to
the stored one and rejecting requests on mismatch or missing state; use the
existing redirect_uri/urlencoded logic and ensure state generation uses a secure
RNG.
| fn load_local_settings(app: &AppHandle) -> Result<UserSettings, String> { | ||
| let path = get_user_settings_path(app)?; | ||
| if !path.exists() { | ||
| return Ok(UserSettings::default()); | ||
| } | ||
| let contents = std::fs::read_to_string(&path).map_err(|e| format!("설정 읽기 실패: {e}"))?; | ||
| serde_json::from_str(&contents).map_err(|e| format!("설정 파싱 실패: {e}")) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
사용자 설정 로컬 저장소 접근을 tauri-plugin-store로 통일해 주세요.
현재 std::fs::read_to_string 기반 직접 파일 접근은 이 레이어의 사용자 설정 저장 규칙과 어긋납니다. 동기화 경로에서도 동일 저장소를 써야 일관성이 유지됩니다.
As per coding guidelines apps/app/src-tauri/src/commands/**/*.rs: Use tauri-plugin-store for user settings (theme, language, salary info)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/app/src-tauri/src/commands/auth.rs` around lines 529 - 536,
load_local_settings currently reads the settings file with std::fs and
serde_json; replace that direct file access with tauri-plugin-store so the app
uses the same store format/rules. In load_local_settings, open the Store for the
path returned by get_user_settings_path (via tauri_plugin_store::Store or
StoreBuilder), call the store load/restore method, and then retrieve/deserialize
the stored UserSettings (e.g., store.get::<UserSettings>("user_settings") or
reading the top-level store value) returning UserSettings::default() when
missing and converting plugin errors into the same Result<String, ...> error
messages you already use. Ensure you reference load_local_settings,
get_user_settings_path, and UserSettings when making the change.
- "계정" 섹션 삭제, "내 정보" 섹션에 로그인/로그아웃 행 통합 - AuthStatus에 provider 필드 추가 (카카오/Apple 연동 표시) - auth.json에 provider 저장, 기존 토큰은 강제 로그아웃 처리 - 로그인 버튼 2개 나란히 배치 (각각 rounded + chevron)
There was a problem hiding this comment.
🧹 Nitpick comments (4)
apps/app/src-tauri/src/commands/auth.rs (3)
582-584: 불완전한 URL 인코딩 함수입니다.현재
urlencoded함수는:와/만 인코딩합니다.redirect_uri가 하드코딩되어 있어 현재는 동작하지만, 향후 다른 특수문자(&,=,?,#, 공백 등)가 포함되면 URL이 깨질 수 있습니다. 표준 URL 인코딩 라이브러리 사용을 권장합니다.🔧 제안 수정
percent-encoding크레이트 사용:+use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; -fn urlencoded(s: &str) -> String { - s.replace(':', "%3A").replace('/', "%2F") -} +fn urlencoded(s: &str) -> String { + utf8_percent_encode(s, NON_ALPHANUMERIC).to_string() +}또는
urlencoding크레이트:-fn urlencoded(s: &str) -> String { - s.replace(':', "%3A").replace('/', "%2F") -} +fn urlencoded(s: &str) -> String { + urlencoding::encode(s).into_owned() +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/commands/auth.rs` around lines 582 - 584, The custom urlencoded(s: &str) function only escapes ':' and '/' and must be replaced with a standard percent-encoding routine to correctly encode characters like &, =, ?, #, space, etc.; locate the urlencoded function and replace its implementation with a call to a URL-encoding library (e.g., percent-encoding's percent_encode or urlencoding::encode) and update imports so all callers (places that call urlencoded) receive properly percent-encoded redirect_uri/query components instead of the fragile manual replace logic.
361-391: 401 에러 발생 후에도 후속 API 호출이 계속 실행됩니다.
handle_api_error가 첫 번째 API 호출(payroll)에서 401을 받아 토큰을 삭제하면, 이후work_policy와payday호출도 이미 삭제된 토큰으로 실행되어 불필요하게 실패합니다. 401 발생 시 조기 반환하는 것이 효율적입니다.🔧 제안 수정
// payroll let payroll_req = PayrollPatchRequest { salary_input_type: to_server_salary_type(&settings.salary_type), salary_amount: settings.salary_amount as i64, }; if let Err(e) = api.patch_payroll(&token, &payroll_req).await { - handle_api_error(&app, &e, "payroll push"); + if matches!(e, ApiError::Unauthorized) { + auth::clear_auth_token(&app); + log::info!("토큰 만료 (payroll push) — 자동 로그아웃"); + return Ok(()); + } + log::warn!("payroll push 실패: {e}"); } // work-policy let work_policy_req = WorkPolicyPatchRequest { ... }; if let Err(e) = api.patch_work_policy(&token, &work_policy_req).await { - handle_api_error(&app, &e, "work-policy push"); + if matches!(e, ApiError::Unauthorized) { + auth::clear_auth_token(&app); + log::info!("토큰 만료 (work-policy push) — 자동 로그아웃"); + return Ok(()); + } + log::warn!("work-policy push 실패: {e}"); } // payday let payday_req = PaydayPatchRequest { ... }; if let Err(e) = api.patch_profile_payday(&token, &payday_req).await { - handle_api_error(&app, &e, "payday push"); + if matches!(e, ApiError::Unauthorized) { + auth::clear_auth_token(&app); + log::info!("토큰 만료 (payday push) — 자동 로그아웃"); + return Ok(()); + } + log::warn!("payday push 실패: {e}"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/commands/auth.rs` around lines 361 - 391, The sequence of API calls (api.patch_payroll, api.patch_work_policy, api.patch_profile_payday) continues even after handle_api_error removes the token on a 401; change the flow so that when handle_api_error indicates an authentication failure (401) you stop further calls and return early: after each await call (especially in the payroll block where token may be invalidated), check the Err result and if handle_api_error reports/represents a 401 scenario (or if the error kind indicates Unauthorized) then return from the surrounding function instead of proceeding to call api.patch_work_policy or api.patch_profile_payday; reference handle_api_error, api.patch_payroll, api.patch_work_policy, api.patch_profile_payday and token to locate and implement the early-return logic.
523-545:push_local_to_server에서 모든 에러가 무시됩니다.세 API 호출 결과가 모두
let _ = ...로 무시되고, 호출자(line 477-478)는 실패 여부와 관계없이 "push 완료"를 로깅합니다. 네트워크 장애 시 실제로는 push가 실패했는데 성공으로 기록되어 디버깅이 어려워질 수 있습니다.🔧 제안 수정
-async fn push_local_to_server(api: &ApiClient, token: &str, settings: &UserSettings) { +async fn push_local_to_server(api: &ApiClient, token: &str, settings: &UserSettings) -> bool { + let mut success = true; let payroll_req = PayrollPatchRequest { ... }; let work_policy_req = WorkPolicyPatchRequest { ... }; let payday_req = PaydayPatchRequest { ... }; - let _ = api.patch_payroll(token, &payroll_req).await; - let _ = api.patch_work_policy(token, &work_policy_req).await; - let _ = api.patch_profile_payday(token, &payday_req).await; + if let Err(e) = api.patch_payroll(token, &payroll_req).await { + log::warn!("payroll push 실패: {e}"); + success = false; + } + if let Err(e) = api.patch_work_policy(token, &work_policy_req).await { + log::warn!("work_policy push 실패: {e}"); + success = false; + } + if let Err(e) = api.patch_profile_payday(token, &payday_req).await { + log::warn!("payday push 실패: {e}"); + success = false; + } + success }호출부:
- push_local_to_server(api, token, s).await; - log::info!("로컬 데이터 → 서버 push 완료"); + if push_local_to_server(api, token, s).await { + log::info!("로컬 데이터 → 서버 push 완료"); + } else { + log::warn!("로컬 데이터 → 서버 push 일부 실패"); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/commands/auth.rs` around lines 523 - 545, push_local_to_server currently ignores all API errors by assigning them to `_`; change it to return a Result and propagate/report failures instead of silencing them: update the signature of push_local_to_server to return a Result<(), _> (or bool) and await the three calls api.patch_payroll, api.patch_work_policy, and api.patch_profile_payday, handling each Result (use `?` to propagate or match and log the error) and return Err on failure so the caller can detect and log a real failure rather than always reporting "push 완료".apps/app/src/features/settings/components/auth-row.tsx (1)
46-72: 로그인 버튼 로딩 상태 UX 개선을 고려해 주세요.두 로그인 버튼이 동일한
isLoginPending플래그로 비활성화됩니다. 사용자가 카카오 버튼을 클릭하면 어떤 로그인이 진행 중인지 시각적으로 구분되지 않습니다. 클릭한 버튼에만 로딩 인디케이터를 표시하면 UX가 개선될 수 있습니다.🔧 제안 수정
Props에 현재 진행 중인 provider 추가:
interface Props { authStatus: AuthStatus | undefined; onLogin: (provider: AuthProvider) => void; onLogout: () => void; - isLoginPending: boolean; + pendingProvider: AuthProvider | null; isLogoutPending: boolean; }버튼에 개별 로딩 상태 적용:
<button type="button" className={...} - disabled={isLoginPending} + disabled={pendingProvider !== null} onClick={() => onLogin('kakao')} > - 카카오로 로그인 + {pendingProvider === 'kakao' ? '로그인 중...' : '카카오로 로그인'} <ChevronRightIcon className="text-text-low size-6" /> </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src/features/settings/components/auth-row.tsx` around lines 46 - 72, The two login buttons share the single isLoginPending flag so users can’t tell which provider is loading; add a prop like activeLoginProvider (string | null) to this component's props, update the onClick handlers to call onLogin(provider) as before but set disabled to (isLoginPending && activeLoginProvider !== provider) or simpler disabled when (isLoginPending && activeLoginProvider !== null) and show a per-button loading indicator when (isLoginPending && activeLoginProvider === provider); update the prop types/usage where AuthRow is used so the caller sets activeLoginProvider to 'kakao' or 'apple' when initiating login and null when finished.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/app/src-tauri/src/commands/auth.rs`:
- Around line 582-584: The custom urlencoded(s: &str) function only escapes ':'
and '/' and must be replaced with a standard percent-encoding routine to
correctly encode characters like &, =, ?, #, space, etc.; locate the urlencoded
function and replace its implementation with a call to a URL-encoding library
(e.g., percent-encoding's percent_encode or urlencoding::encode) and update
imports so all callers (places that call urlencoded) receive properly
percent-encoded redirect_uri/query components instead of the fragile manual
replace logic.
- Around line 361-391: The sequence of API calls (api.patch_payroll,
api.patch_work_policy, api.patch_profile_payday) continues even after
handle_api_error removes the token on a 401; change the flow so that when
handle_api_error indicates an authentication failure (401) you stop further
calls and return early: after each await call (especially in the payroll block
where token may be invalidated), check the Err result and if handle_api_error
reports/represents a 401 scenario (or if the error kind indicates Unauthorized)
then return from the surrounding function instead of proceeding to call
api.patch_work_policy or api.patch_profile_payday; reference handle_api_error,
api.patch_payroll, api.patch_work_policy, api.patch_profile_payday and token to
locate and implement the early-return logic.
- Around line 523-545: push_local_to_server currently ignores all API errors by
assigning them to `_`; change it to return a Result and propagate/report
failures instead of silencing them: update the signature of push_local_to_server
to return a Result<(), _> (or bool) and await the three calls api.patch_payroll,
api.patch_work_policy, and api.patch_profile_payday, handling each Result (use
`?` to propagate or match and log the error) and return Err on failure so the
caller can detect and log a real failure rather than always reporting "push 완료".
In `@apps/app/src/features/settings/components/auth-row.tsx`:
- Around line 46-72: The two login buttons share the single isLoginPending flag
so users can’t tell which provider is loading; add a prop like
activeLoginProvider (string | null) to this component's props, update the
onClick handlers to call onLogin(provider) as before but set disabled to
(isLoginPending && activeLoginProvider !== provider) or simpler disabled when
(isLoginPending && activeLoginProvider !== null) and show a per-button loading
indicator when (isLoginPending && activeLoginProvider === provider); update the
prop types/usage where AuthRow is used so the caller sets activeLoginProvider to
'kakao' or 'apple' when initiating login and null when finished.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4aa4bf0-3285-40b6-88bd-78fc8325695f
📒 Files selected for processing (6)
apps/app/src-tauri/src/auth.rsapps/app/src-tauri/src/commands/auth.rsapps/app/src/features/settings/components/auth-row.tsxapps/app/src/features/settings/screens/settings-screen.tsxapps/app/src/lib/tauri-bindings.gen.tsapps/app/src/ui/info-row.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/app/src/ui/info-row.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/app/src/features/settings/screens/settings-screen.tsx
- apps/app/src-tauri/src/auth.rs
- apps/app/src/lib/tauri-bindings.gen.ts
CodeRabbit 리뷰 대응
|
- accept() 타임아웃: set_ttl (IP TTL) → non-blocking polling + 2분 deadline - 서버 응답 캐스팅: as → try_from + 범위 검증 (salary_amount, payday_day) - 주기적 sync: AtomicBool in-flight 가드로 중첩 실행 방지 - 로그인 에러 UI 표시 및 디버그 로그 추가
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/app/src-tauri/src/salary.rs (1)
79-105: 중첩 실행 방지 로직이 적절히 구현되었습니다.
AtomicBool+compare_exchange패턴으로 이전 리뷰에서 지적된 동시 실행 문제가 해결되었습니다. 초기화 대기(10초) 후 5분 주기 폴링 구조도 적절합니다.선택적 개선 사항:
sync_from_server가 panic하면SYNC_IN_FLIGHT가 영원히true로 남아 이후 모든 sync가 차단될 수 있습니다. 현실적으로 panic 가능성이 낮다면 현재 구현으로 충분하지만, 방어적으로 guard 패턴을 고려할 수 있습니다.🛡️ Panic-safe guard 예시 (선택 사항)
+struct SyncGuard; +impl Drop for SyncGuard { + fn drop(&mut self) { + SYNC_IN_FLIGHT.store(false, Ordering::SeqCst); + } +} + if SYNC_IN_FLIGHT .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_ok() { let app_clone = app.clone(); tauri::async_runtime::spawn(async move { + let _guard = SyncGuard; let _ = crate::commands::auth::sync_from_server(app_clone).await; - SYNC_IN_FLIGHT.store(false, Ordering::SeqCst); }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/salary.rs` around lines 79 - 105, The current compare_exchange guard (SYNC_IN_FLIGHT) in start_salary_ticker prevents concurrent runs but never resets if the async task panics, which would permanently block future syncs; ensure SYNC_IN_FLIGHT is always cleared by wrapping the async work started in tauri::async_runtime::spawn with a panic-safe guard or unwind-catching logic that resets SYNC_IN_FLIGHT on both success and panic—e.g., inside the spawned async closure create a small guard type whose Drop calls SYNC_IN_FLIGHT.store(false, Ordering::SeqCst) or run sync_from_server(app_clone).await inside std::panic::catch_unwind/AssertUnwindSafe and then store(false) in the finally path so that sync_from_server and any panic both restore the flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/app/src-tauri/src/salary.rs`:
- Around line 79-105: The current compare_exchange guard (SYNC_IN_FLIGHT) in
start_salary_ticker prevents concurrent runs but never resets if the async task
panics, which would permanently block future syncs; ensure SYNC_IN_FLIGHT is
always cleared by wrapping the async work started in tauri::async_runtime::spawn
with a panic-safe guard or unwind-catching logic that resets SYNC_IN_FLIGHT on
both success and panic—e.g., inside the spawned async closure create a small
guard type whose Drop calls SYNC_IN_FLIGHT.store(false, Ordering::SeqCst) or run
sync_from_server(app_clone).await inside
std::panic::catch_unwind/AssertUnwindSafe and then store(false) in the finally
path so that sync_from_server and any panic both restore the flag.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7da0f47b-eeaf-4ab0-b75f-a0d009f65a4b
📒 Files selected for processing (3)
apps/app/src-tauri/src/commands/auth.rsapps/app/src-tauri/src/salary.rsapps/app/src/features/onboarding/screens/welcome-screen.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/app/src/features/onboarding/screens/welcome-screen.tsx
- apps/app/src-tauri/src/commands/auth.rs
- 브라우저 완료 페이지 카드 UI + window.close() 제거 - OAuth 완료 후 앱 패널 자동 표시 (show_main_window) - 로그인 진행 중 "브라우저에서 로그인을 완료해 주세요" 안내 추가
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/app/src/features/onboarding/screens/welcome-screen.tsx (1)
68-87: 로그인 버튼 pending 상태 표시가 일관되지 않습니다.카카오 버튼은 pending 시 "로그인 중..."으로 변경되지만, Apple 버튼은 그대로 유지됩니다. 사용자 혼란 방지를 위해 클릭된 버튼만 텍스트를 변경하거나, 둘 다 일관되게 처리하는 것을 고려해 보세요.
💡 클릭된 버튼만 로딩 표시하는 방법
+ const [pendingProvider, setPendingProvider] = useState<'kakao' | 'apple' | null>(null); const handleSocialLogin = (provider: 'kakao' | 'apple') => { + setPendingProvider(provider); socialLogin.mutate(provider, { onSuccess: (result) => { + setPendingProvider(null); // ... existing code }, onError: (error) => { + setPendingProvider(null); console.error('소셜 로그인 실패:', error); }, }); }; // 버튼에서 - {socialLogin.isPending ? '로그인 중...' : '카카오로 시작하기'} + {pendingProvider === 'kakao' ? '로그인 중...' : '카카오로 시작하기'}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src/features/onboarding/screens/welcome-screen.tsx` around lines 68 - 87, The two social login buttons show inconsistent loading text because only the Kakao button checks socialLogin.isPending; update the state logic to track which provider is pending (e.g., extend socialLogin to include provider or add pendingProvider) and change the Button label and disabled check to conditionally show "로그인 중..." only when socialLogin.isPending && socialLogin.provider === 'kakao' for the Kakao Button and similarly check provider === 'apple' for the Apple Button; update handleSocialLogin to set the pending provider on start and clear it when finished so the correct Button displays the loading text.apps/app/src-tauri/src/commands/auth.rs (1)
600-602:urlencoded함수가 일부 특수문자만 처리합니다.현재
:와/만 인코딩하고 있습니다.redirect_uri에 다른 특수문자가 포함될 경우를 대비해urlencoding크레이트 사용을 고려해 보세요.🔧 더 완전한 URL 인코딩 제안
-fn urlencoded(s: &str) -> String { - s.replace(':', "%3A").replace('/', "%2F") -} +fn urlencoded(s: &str) -> String { + // 현재 redirect_uri는 http://127.0.0.1:17171/callback 형식이므로 + // 기본 특수문자만 처리해도 충분하지만, 향후 확장 시 고려 필요 + s.replace(':', "%3A").replace('/', "%2F") +}또는
urlencoding크레이트를 사용:use urlencoding::encode; let encoded = encode(&redirect_uri);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/app/src-tauri/src/commands/auth.rs` around lines 600 - 602, The custom urlencoded(s: &str) function only replaces ':' and '/' and should be replaced with a proper percent-encoding call; update urlencoded to use a standard encoder (for example urlencoding::encode or percent_encoding::utf8_percent_encode) so all reserved and non-ASCII chars are handled correctly, add the corresponding use/import, and return the encoded String from the same urlencoded function (referencing urlencoded to locate where to change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/app/src-tauri/src/commands/auth.rs`:
- Around line 600-602: The custom urlencoded(s: &str) function only replaces ':'
and '/' and should be replaced with a proper percent-encoding call; update
urlencoded to use a standard encoder (for example urlencoding::encode or
percent_encoding::utf8_percent_encode) so all reserved and non-ASCII chars are
handled correctly, add the corresponding use/import, and return the encoded
String from the same urlencoded function (referencing urlencoded to locate where
to change).
In `@apps/app/src/features/onboarding/screens/welcome-screen.tsx`:
- Around line 68-87: The two social login buttons show inconsistent loading text
because only the Kakao button checks socialLogin.isPending; update the state
logic to track which provider is pending (e.g., extend socialLogin to include
provider or add pendingProvider) and change the Button label and disabled check
to conditionally show "로그인 중..." only when socialLogin.isPending &&
socialLogin.provider === 'kakao' for the Kakao Button and similarly check
provider === 'apple' for the Apple Button; update handleSocialLogin to set the
pending provider on start and clear it when finished so the correct Button
displays the loading text.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a4c59e70-6619-4985-ba29-25924ed08ef8
📒 Files selected for processing (5)
apps/app/src-tauri/src/commands/auth.rsapps/app/src-tauri/src/lib.rsapps/app/src-tauri/src/tray.rsapps/app/src/features/onboarding/screens/welcome-screen.tsxapps/app/src/features/settings/components/auth-row.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/app/src-tauri/src/tray.rs
루트 .env에 통합 관리, 파편화 방지
콘솔에서 "사용함" 설정 시 필수 파라미터
- 돈 배경 이미지 + MOA 로고 + 부제 레이아웃으로 전면 변경 - 카카오/애플 소셜 로그인 버튼에 아이콘 추가 - 기존 TooltipBubble + HeroIcon + 급여 표시 제거
브라우저를 닫아도 2분간 로딩에 갇히는 문제 해결. AtomicBool 취소 플래그 + cancel_social_login 커맨드 추가, 로그인 버튼 재클릭 시 이전 세션 취소 후 새 세션 시작.
- WelcomeScreen을 onboarding에서 분리하여 독립 LoginScreen(features/auth) 생성 - /login 라우트 추가, returnTo search param으로 로그인 후 복귀 위치 제어 - 설정 화면 로그인 영역을 Figma 디자인(녹색 "로그인 · 회원가입하기" 링크)으로 변경 - 섹션 타이틀 "내 정보" → "내 계정" 변경 - 온보딩 SCREEN_ORDER에서 welcome 제거
- chevron 화살표 색상을 흰색(text-text-high)으로 변경 - "내 정보" 서브타이틀을 별도 섹션으로 분리하여 "월급 · 근무 정보" 상단에 추가 - "내 계정"과 "내 정보" 사이 간격 24px(gap-6)로 조정 - "내 계정" 타이틀을 b2-400으로, 타이틀-콘텐츠 간격 4px(gap-1)로 변경
로그인: 로그아웃 + 회원탈퇴(TODO) 비로그인: 데이터 초기화 + 앱 종료하기
Summary
Kakao/Apple OAuth 소셜 로그인 기능을 추가하고, 로그인 완료 시 MOA 서버의 급여 정보와 로컬 데이터를 동기화합니다. 로그인하지 않은 사용자는 기존 로컬 데이터로 앱을 사용할 수 있습니다.
Why
사용자 온보딩 개선
Before:
After:
Implementation
백엔드 (Rust/Tauri):
auth.json(Tauri secure storage)프론트엔드 (React):
WelcomeScreen: Kakao/Apple 로그인 버튼 + "로그인 없이 시작"useAuthhook: 토큰 상태 관리 (TanStack Query)SettingsScreen: 계정 정보 표시 및 로그아웃sync_settings_to_server호출충돌 해결:
menubar_display_mode,menubar_icon_theme) 유지Impact & Testing
Risk: 토큰 저장 실패 시 서비스 이용 불가 → fallback: 로컬 모드만 작동
Changes:
KAKAO_REST_API_KEY,APPLE_CLIENT_ID/TEAM_ID/KEY_ID/PRIVATE_KEYPOST /api/v1/auth/kakao,POST /api/v1/auth/apple,GET /api/v1/onboarding/statusTesting:
bun dev:app실행settings.json데이터가 서버 값으로 동기화 확인Commits
Summary by CodeRabbit