Skip to content

feat: Kakao/Apple 소셜 로그인 및 서버 API 동기화#92

Open
bepyan wants to merge 17 commits intomainfrom
feature/kakao-login-sync
Open

feat: Kakao/Apple 소셜 로그인 및 서버 API 동기화#92
bepyan wants to merge 17 commits intomainfrom
feature/kakao-login-sync

Conversation

@bepyan
Copy link
Copy Markdown
Collaborator

@bepyan bepyan commented Mar 18, 2026

Summary

Kakao/Apple OAuth 소셜 로그인 기능을 추가하고, 로그인 완료 시 MOA 서버의 급여 정보와 로컬 데이터를 동기화합니다. 로그인하지 않은 사용자는 기존 로컬 데이터로 앱을 사용할 수 있습니다.

Why

사용자 온보딩 개선

Before:

  • 로컬 데이터 입력만 가능 → 계정 연동 불가
  • 데스크톱/모바일 간 데이터 공유 불가

After:

  • Kakao/Apple 계정으로 로그인 → MOA 서버 데이터 자동 동기화
  • 여러 기기에서 일관된 데이터 유지
  • "로그인 없이 시작" 옵션으로 기존 플로우 유지

Implementation

백엔드 (Rust/Tauri):

  • OAuth callback 처리: localhost:17171 TCP 서버로 redirect_uri 수신
  • Kakao/Apple auth code → id_token 교환 (각 OAuth 제공자 REST API)
  • id_token → MOA accessToken 발급 (MOA 서버 인증)
  • 토큰 저장: auth.json (Tauri secure storage)
  • 데이터 싱크: 3중 트리거 (패널 열 때, 5분 polling, 앱 시작 시)

프론트엔드 (React):

  • WelcomeScreen: Kakao/Apple 로그인 버튼 + "로그인 없이 시작"
  • useAuth hook: 토큰 상태 관리 (TanStack Query)
  • SettingsScreen: 계정 정보 표시 및 로그아웃
  • 설정 저장 시 자동으로 sync_settings_to_server 호출

충돌 해결:

  • 서버 데이터 우선, 로컬 전용 필드(menubar_display_mode, menubar_icon_theme) 유지
  • 변경 감지 후 디스크에만 기록 (불필요한 I/O 방지)

Impact & Testing

Risk: 토큰 저장 실패 시 서비스 이용 불가 → fallback: 로컬 모드만 작동

Changes:

  • 새 환경변수: KAKAO_REST_API_KEY, APPLE_CLIENT_ID/TEAM_ID/KEY_ID/PRIVATE_KEY
  • UI 변화: Welcome 화면에 소셜 로그인 버튼
  • 새 API 호출: POST /api/v1/auth/kakao, POST /api/v1/auth/apple, GET /api/v1/onboarding/status

Testing:

  • Kakao 개발자 콘솔에서 REST API Key 설정 후 bun dev:app 실행
  • Apple은 Apple Developer 계정 필요 (별도 설정)
  • 로그인 후 settings.json 데이터가 서버 값으로 동기화 확인

Commits

  • d86c01a: 소셜 로그인/동기화 구현
  • 865ecbe: OAuth 포트를 17171로 고정

Summary by CodeRabbit

  • 새로운 기능
    • 카카오·애플 소셜 로그인 및 온보딩 연계
    • 설정 화면에 로그인 상태 표시와 연동/로그아웃 UI 추가
    • 로그인/설정 변경 시 서버와 비동기 자동 동기화(백그라운드) 추가
    • 패널 표시 이벤트로 서버 동기화 트리거 추가
    • 주기적 백그라운드 급여 정보 동기화(티커) 추가
    • 앱용 환경 변수 예제(.env.example) 추가

bepyan added 2 commits March 18, 2026 23:51
- Rust: auth.rs (토큰 관리), api_client.rs (서버 통신), commands/auth.rs (OAuth 플로우)
- 로그인 시 서버→로컬 데이터 sync, 설정 변경 시 로컬→서버 push
- 패널 열 때 + 5분 주기 polling으로 서버 데이터 자동 pull
- 온보딩 welcome 화면에 소셜 로그인 버튼 추가
- 설정 화면에 계정 섹션(로그인/로그아웃) 추가
- 비로그인 시 기존 로컬 전용 동작 유지
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

Warning

Rate limit exceeded

@bepyan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 3 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 603db354-7092-4e40-ba51-2f8a8580fde5

📥 Commits

Reviewing files that changed from the base of the PR and between 36e6db4 and 51734aa.

⛔ Files ignored due to path filters (2)
  • apps/app/src-tauri/Cargo.lock is excluded by !**/*.lock
  • apps/app/src/assets/money-bg.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • apps/app/src-tauri/Cargo.toml
  • apps/app/src-tauri/src/bindings.rs
  • apps/app/src-tauri/src/commands/auth.rs
  • apps/app/src-tauri/src/salary.rs
  • apps/app/src-tauri/src/tray.rs
  • apps/app/src/features/auth/screens/login-screen.tsx
  • apps/app/src/features/onboarding/hooks/use-onboarding-screen.ts
  • apps/app/src/features/onboarding/screens/welcome-screen.tsx
  • apps/app/src/features/settings/components/auth-row.tsx
  • apps/app/src/features/settings/screens/settings-screen.tsx
  • apps/app/src/global.css
  • apps/app/src/hooks/use-auth.ts
  • apps/app/src/lib/tauri-bindings.gen.ts
  • apps/app/src/queries/auth.ts
  • apps/app/src/routeTree.gen.ts
  • apps/app/src/routes/index.tsx
  • apps/app/src/routes/login.tsx
  • apps/app/src/routes/onboarding/welcome.tsx
  • apps/app/src/ui/icons/apple-logo.tsx
  • apps/app/src/ui/icons/edit.tsx
  • apps/app/src/ui/icons/index.ts
  • apps/app/src/ui/icons/kakao-logo.tsx

Walkthrough

Kakao·Apple 소셜 로그인, 토큰 영구 저장 및 MOA 서버와의 양방향 설정 동기화가 추가되었고, 이를 위한 Rust API 클라이언트·인증 모듈·주기 동기화와 프론트엔드 바인딩·훅·UI 요소가 연동되었습니다. 환경 예시 및 몇몇 의존성도 추가되었습니다.

Changes

Cohort / File(s) Summary
Rust: API 클라이언트 및 인증 상태
apps/app/src-tauri/src/api_client.rs, apps/app/src-tauri/src/auth.rs, apps/app/src-tauri/Cargo.toml, apps/app/src-tauri/.env.example, apps/app/src-tauri/src/lib.rs
reqwest 기반 ApiClient와 ApiError/DTO 추가, AuthStore/AuthState 및 디스크 영속화 로직 추가, dotenvy/jsonwebtoken/open 등 의존성 추가, env 예시 파일 추가, 앱 시작 시 AuthStore 등록 및 토큰 복원.
Rust: 인증 커맨드·동기화·주기 작업
apps/app/src-tauri/src/commands/auth.rs, apps/app/src-tauri/src/commands/mod.rs, apps/app/src-tauri/src/salary.rs, apps/app/src-tauri/src/tray.rs, apps/app/src-tauri/src/bindings.rs
로컬 콜백을 통한 OAuth 흐름 구현(Kakao/Apple), Apple JWT 생성 및 토큰 교환, 서버 로그인 및 토큰 저장, sync_from_server / sync_settings_to_server 명령 구현, start_salary_ticker에 주기 동기화 스레드 추가, 패널 표시 시 "panel-shown" 이벤트 방출, 바인딩에 auth 명령 등록.
TypeScript: 바인딩·쿼리·훅
apps/app/src/lib/tauri-bindings.gen.ts, apps/app/src/lib/tauri-bindings.ts, apps/app/src/queries/auth.ts, apps/app/src/queries/index.ts, apps/app/src/hooks/use-auth.ts
새 Tauri 명령(socialLogin, logout, getAuthStatus, syncSettingsToServer, syncFromServer)과 타입(AuthProvider/AuthStatus/LoginResult) 래퍼 추가, react-query용 auth 쿼리 및 훅 구현(로그인/로그아웃 뮤테이션 포함).
프론트엔드 UI/화면 변경
apps/app/src/features/onboarding/screens/welcome-screen.tsx, apps/app/src/features/onboarding/hooks/use-onboarding-form.ts, apps/app/src/features/settings/screens/settings-screen.tsx, apps/app/src/features/settings/hooks/use-settings-form.ts, apps/app/src/features/root-layout.tsx, apps/app/src/features/settings/components/auth-row.tsx
Welcome에 Kakao/Apple 로그인 버튼 추가 및 소셜 로그인 흐름 연결, 온보딩/설정 저장 후 비동기 서버 동기화(fire-and-forget) 호출 추가, Settings에 AuthRow 삽입, 루트 레이아웃에서 panel-shown 수신 시 서버 동기화 트리거.
UI 유틸·타입 내보내기
apps/app/src/ui/info-row.tsx, apps/app/src/lib/tauri-bindings.gen.ts
infoRowVariants를 export로 변경; tauri 바인딩에 신규 명령용 TS 래퍼 및 타입 선언 추가.

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 반환
Loading
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: 패치 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 달빛 아래 토큰 하나 끌어안고,
카카오와 애플 손잡고 깡충깡충,
서버와 로컬이 속삭이며 맞추고,
설정은 조용히 춤추며 동기화되네.
당근 물고 기쁘게 뛰어라, 냠냠 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경사항의 핵심 기능을 정확하게 반영합니다. Kakao/Apple 소셜 로그인과 서버 API 동기화라는 주요 변경사항이 명확하게 표현되어 있습니다.
Docstring Coverage ✅ Passed Docstring coverage is 82.14% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/kakao-login-sync

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
apps/app/src-tauri/src/auth.rs (2)

18-23: 토큰 저장 보안 고려사항.

auth.jsonaccess_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

📥 Commits

Reviewing files that changed from the base of the PR and between bcb2e24 and 865ecbe.

⛔ Files ignored due to path filters (1)
  • apps/app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • apps/app/src-tauri/.env.example
  • apps/app/src-tauri/Cargo.toml
  • apps/app/src-tauri/src/api_client.rs
  • apps/app/src-tauri/src/auth.rs
  • apps/app/src-tauri/src/bindings.rs
  • apps/app/src-tauri/src/commands/auth.rs
  • apps/app/src-tauri/src/commands/mod.rs
  • apps/app/src-tauri/src/lib.rs
  • apps/app/src-tauri/src/salary.rs
  • apps/app/src-tauri/src/tray.rs
  • apps/app/src/features/onboarding/hooks/use-onboarding-form.ts
  • apps/app/src/features/onboarding/screens/welcome-screen.tsx
  • apps/app/src/features/root-layout.tsx
  • apps/app/src/features/settings/hooks/use-settings-form.ts
  • apps/app/src/features/settings/screens/settings-screen.tsx
  • apps/app/src/hooks/use-auth.ts
  • apps/app/src/lib/tauri-bindings.gen.ts
  • apps/app/src/lib/tauri-bindings.ts
  • apps/app/src/queries/auth.ts
  • apps/app/src/queries/index.ts

Comment thread apps/app/src-tauri/src/commands/auth.rs Outdated
Comment on lines +244 to +263
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),
)
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment thread apps/app/src-tauri/src/commands/auth.rs Outdated
Comment on lines +529 to +536
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}"))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread apps/app/src-tauri/src/salary.rs
Comment thread apps/app/src/features/settings/screens/settings-screen.tsx Outdated
- "계정" 섹션 삭제, "내 정보" 섹션에 로그인/로그아웃 행 통합
- AuthStatus에 provider 필드 추가 (카카오/Apple 연동 표시)
- auth.json에 provider 저장, 기존 토큰은 강제 로그아웃 처리
- 로그인 버튼 2개 나란히 배치 (각각 rounded + chevron)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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_policypayday 호출도 이미 삭제된 토큰으로 실행되어 불필요하게 실패합니다. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 865ecbe and 7cd5046.

📒 Files selected for processing (6)
  • apps/app/src-tauri/src/auth.rs
  • apps/app/src-tauri/src/commands/auth.rs
  • apps/app/src/features/settings/components/auth-row.tsx
  • apps/app/src/features/settings/screens/settings-screen.tsx
  • apps/app/src/lib/tauri-bindings.gen.ts
  • apps/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

@bepyan
Copy link
Copy Markdown
Collaborator Author

bepyan commented Mar 19, 2026

CodeRabbit 리뷰 대응

# 이슈 판정 조치
#2 set_ttl(120) ≠ accept timeout ✅ 수정 non-blocking polling + 2분 deadline으로 교체
#3 OAuth state CSRF ⏭️ 스킵 localhost 데스크톱 앱 — 공격 벡터 제한적
#4 as 캐스팅 truncation ✅ 수정 try_from + 범위 검증, 실패 시 기존값 유지
#5 tauri-plugin-store 사용 ⏭️ 스킵 코드베이스 전체가 std::fs + atomic write 패턴 사용, store 플러그인 미도입
#6 sync 중첩 실행 ✅ 수정 AtomicBool in-flight 가드 추가

- accept() 타임아웃: set_ttl (IP TTL) → non-blocking polling + 2분 deadline
- 서버 응답 캐스팅: as → try_from + 범위 검증 (salary_amount, payday_day)
- 주기적 sync: AtomicBool in-flight 가드로 중첩 실행 방지
- 로그인 에러 UI 표시 및 디버그 로그 추가
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd5046 and 4571a7e.

📒 Files selected for processing (3)
  • apps/app/src-tauri/src/commands/auth.rs
  • apps/app/src-tauri/src/salary.rs
  • apps/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

bepyan added 2 commits March 19, 2026 21:51
- 브라우저 완료 페이지 카드 UI + window.close() 제거
- OAuth 완료 후 앱 패널 자동 표시 (show_main_window)
- 로그인 진행 중 "브라우저에서 로그인을 완료해 주세요" 안내 추가
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4571a7e and 36e6db4.

📒 Files selected for processing (5)
  • apps/app/src-tauri/src/commands/auth.rs
  • apps/app/src-tauri/src/lib.rs
  • apps/app/src-tauri/src/tray.rs
  • apps/app/src/features/onboarding/screens/welcome-screen.tsx
  • apps/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

bepyan added 11 commits March 19, 2026 21:57
루트 .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)
비로그인: 데이터 초기화 + 앱 종료하기
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant