Skip to content

[release] v1.4.1 - 2026-05-27#363

Merged
sterdsterd merged 164 commits into
mainfrom
qa
May 27, 2026
Merged

[release] v1.4.1 - 2026-05-27#363
sterdsterd merged 164 commits into
mainfrom
qa

Conversation

@sterdsterd
Copy link
Copy Markdown
Collaborator

Summary

Linear

Changes

Testing

Risk / Impact

  • 영향 범위:
  • 확인이 필요한 부분:
  • 배포 시 유의사항:

Screenshots / Video

sterdsterd and others added 30 commits May 20, 2026 03:50
Addresses several pre-existing infrastructure drift issues:

1. Admin typecheck silent pass (MAT-655)
   - apps/admin/package.json: typecheck "tsc --noEmit" -> "tsc -b --noEmit"
     so composite references (tsconfig.app.json + tsconfig.node.json) are
     actually checked.
   - Fix 6 accumulated TS errors the silent script was hiding:
     * EditConceptModal MouseEvent generic widened to HTMLElement
     * adminPermissions.ts non-null assertion after length guard
     * useInvalidate invalidateNotice/MockExamResults use array-prefix
       queryKey for partial-match invalidation without studentId param
     * useProblemEssentialInput ProblemType -> CreateType ('GICHUL_PROBLEM'
       belongs to CreateType, not ProblemType)

2. Native lint missing from CI
   - package.json ci:lint switched from filtered list to plain
     "turbo lint" so every workspace runs (including native expo lint).
   - Auto-fix 33 native lint errors surfaced by new enforcement.

3. Prettier plugin version drift
   - apps/native/.prettierrc added so prettier nearest-wins config
     picks up native's local prettier-plugin-tailwindcss@0.5.14
     (NativeWind 4 / Tailwind v3) instead of root v0.6.14 (admin
     Tailwind v4).

4. Turbo pipeline inputs
   - turbo.json lint/typecheck declare explicit inputs (eslint, prettier,
     tsconfig) so cache invalidates on config changes.

Verification:
- pnpm typecheck PASS (5/5 workspaces)
- pnpm ci:lint PASS (5/5, native now included)
- pnpm format:check PASS
CI typecheck was failing after admin tsc -b enforcement because
admin depends on:
- @repo/pointer-editor-v2 dist/ (workspace package, not built by pnpm install)
- routeTree.gen.ts (generated by @tanstack/router-plugin during vite build)

Changes:
- turbo.json typecheck: dependsOn ['^typecheck', '^build'] so workspace
  packages build their dist/ before any typecheck runs.
- apps/admin typecheck script: 'tsr generate && tsc -b --noEmit' so
  routeTree.gen.ts is regenerated before composite typecheck runs.
- Add @tanstack/router-cli as devDep for tsr CLI.
- Remove invalidateNotice (dead code, zero callers) from useInvalidate
  to avoid the array-prefix queryKey shim that broke pattern parity
  with the rest of the file.
…eck-format-infra

[chore/MAT-655] monorepo lint/typecheck/format 인프라 정합
- app.config.ts: production env fail-fast (NATIVE_API_BASE_URL, GOOGLE_*, KAKAO_NATIVE_APP_KEY + google-services files), aps-environment per EAS_BUILD_PROFILE
- iOS infoPlist: LSMinimumSystemVersion 15.1, Korean usage descriptions (Camera/PhotoLibrary/PhotoLibraryAdd)
- Android: explicit permission whitelist (android.permissions: []) to exclude unused READ/WRITE_EXTERNAL_STORAGE, RECORD_AUDIO, SYSTEM_ALERT_WINDOW
- withPrivacyManifest config plugin: PrivacyInfo.xcprivacy with 8 collected data types (Name, Email, PhoneNumber, SensitiveInfo, UserID, ProductInteraction, OtherDiagnosticData, PhotosorVideos — all linked/non-tracking) + 4 Required Reason APIs
- eas.json: distribution=store and developmentClient=false for production profile, APP_VARIANT env per profile
- package.json: align SDK 54 dep versions via expo install --fix
Three locations were over multiple lines but fit within printWidth 100:
import withDangerousMod/withAndroidManifest, NSCameraUsageDescription
string literal, and the final default export composition.
QnA/Scrap는 1.0 출시 시점에 비활성이므로 해당 기능에서만 수집/사용
되는 항목을 Privacy Manifest / Info.plist에서 제거:

- NSPrivacyCollectedDataTypes
  * PhotosorVideos (QnA/Scrap 업로드에서만 수집)
  * SensitiveInfo (실제 직접 수집 코드 없음)
- Info.plist
  * NSPhotoLibraryAddUsageDescription (saveToLibrary 호출 경로 0건)

NSCameraUsageDescription, NSPhotoLibraryUsageDescription은
expo-image-picker API가 binary에 포함되어 있어 missing key 회피
차원에서 유지. 차후 QnA/Scrap 활성화 시 PhotosorVideos / Add
권한도 함께 재선언.
OAuth 응답에서 이메일을 받았을 때 SignupEmail 스텝을 건너뛰도록 회원가입 내비게이션을 정리한다.

- useSignupStore: persist whitelist(partialize) 도입 + version/migrate 추가, skipEmailStep 플래그 신설 (앱 종료/재구동 시에도 상태 복원)
- AuthNavigator: getSignupInitialRoute가 skipEmailStep을 우선 평가하여 SignupTerms로 진입
- SignupEmailScreen: mount guard 추가 — skipEmailStep=true 이면 SignupTerms로 즉시 replace (deep link/외부 navigate 보호)
- SignupTermsScreen: 뒤로가기 분기 — skipEmailStep=true 이면 signOut+Login reset, 아니면 기존 goBack 유지
- tsconfig: @lib alias 추가 (이어지는 commit에서 사용)
로그아웃·탈퇴·refresh 흐름에서 다음 사용자의 데이터 잔존/잘못된 푸시/잘못된 인증 유지 문제를 차단한다.

- src/lib/queryClient.ts: App.tsx 인라인 QueryClient를 모듈로 추출하여 외부에서 cache clear 가능하게 함
- authStore: performSessionCleanup(unregister FCM → SecureStore clear → queryClient.clear) 도입, signOut/세션 인증 실패 경로 모두 동일 루틴 사용 (MAT-773, 753)
- authStore: refresh 성공 후 /me 4xx 응답을 인증 실패로 확정 처리 (이전엔 token 응답의 name/grade로 fake-valid) (MAT-748)
- useFcmToken: 자동 권한 요청 제거 (권한은 명시적 동의 화면으로 이동 예정), unregisterFcmToken() 헬퍼 export — deleteToken() + postPushToken('') 둘 다 호출
- WithdrawalScreen: openapi-fetch { data, error } 패턴으로 변경, error 발생 시 signOut 호출하지 않고 안내만 표시 (MAT-751)
- SignupIdentityScreen: postPhoneSend/Verify/Resend 응답에 data?.success === false 가드 추가 (방어적, 미래 변경 대비) (MAT-750)
useNativeOAuth가 OAuth 응답을 받았을 때:
- 이메일이 있으면 signupStore.skipEmailStep=true 세팅 후 SignupTerms로 이동 (MAT-764)
- SDK/네트워크 에러는 mapOAuthErrorToUserMessage로 정제된 한국어 문구만 노출, 사용자 취소(Apple/Google/Kakao 공통)는 조용히 종료 (MAT-774)
- SDK signOut 실패는 silent swallow 대신 console.warn 처리 — 다음 로그인 시 SDK가 재초기화 (MAT-775)

신규 파일 src/features/auth/login/utils/oauthErrorMessage.ts에 isOAuthCancelError + mapOAuthErrorToUserMessage 공통 헬퍼 분리.
…hardening

[feat/MAT-746] 학생앱 심사 대응 auth/session 안정화
…sion-config

[chore/MAT-746] 학생앱 심사 native 설정 통합
iPhone 14 Pro/Dynamic Island 등 notched 기기에서 토스트가 status bar에 잘리는 문제 해결.
- showToast: 모듈 레벨 latched topOffset 도입 (ToastSafeAreaBridge가 SafeAreaProvider 내부에서 inset 추적)
- useToast hook 추가 (호출 컴포넌트가 Provider 안에 있을 때 사용 가능)
- App.tsx: SafeAreaProvider 직속에 ToastSafeAreaBridge mount
학생앱 심사 정책 정합성:
- src/constants/termsUrls.ts: 약관/개인정보/마케팅 Notion URL을 단일 source로 추출
- tsconfig.json: @constants 별칭 추가
- SignupTermsScreen: TERMS_URLS을 새 상수에서 import (구 in-file 상수 제거)
- TermsScreen(메뉴): handleTermPress 활성화 — in-app WebBrowser로 Notion 문서 오픈
- MenuScreen: 고객센터 진입 시 mailto:develop@math-pointer.com 메일 컴포저 (학생앱 문의 prefilled)
QnA 채팅 알림은 정식 출시 전까지 비활성:
- putAllowPush.ts: QNA_PUSH_DISABLED 상수 + sanitizePushSettings 헬퍼 추출.
  usePutAllowPush의 mutationFn에서 자동으로 isAllowQnaPush:false 동봉
- NotificationSettingsScreen: QnA 토글 UI/state/handler 제거(주석 자리만 유지),
  자동 push-on 분기 및 모든 PUT 호출에서 isAllowQnaPush:false 강제
학생앱 심사 권한 요청 타이밍 정합 — onboarding 마지막 단계 직전에 PushConsentStep 신설:
- PushConsentStep: 3-phase UI (request → tune → denied)
  · Phase request: "알림 허용" CTA → messaging().requestPermission()
  · Phase tune: 서비스(default ON)/이벤트 마케팅(default OFF) 토글 2개,
    "완료" 시 PUT /api/student/me/push/settings + FCM 토큰 등록
  · Phase denied: OS 권한 거부 시 "설정 앱으로 이동" CTA (Linking.openSettings)
  · "다음에 받기" 선택 시 서버 호출 0, FCM 토큰 등록 0
- OnboardingStackParamList에 PushConsent route 추가, OnboardingScreen에 등록
- useFinishOnboarding: register 성공 후 Welcome 대신 PushConsent로 reset
- useOnboardingResume: PushConsent는 Welcome과 동일하게 early-return
…licy-push

[feat/MAT-746] 학생앱 심사 onboarding/policy/push 정합
백엔드 응답 user.email이 카카오/구글 케이스에서 null로 와 SignupEmail
스텝이 노출되는 회귀(MAT-827)를 클라이언트 측 fallback으로 차단.

- getGoogle/Kakao/AppleToken을 {token, email} 반환으로 변경
- 카카오: login({ scopes: ['account_email'] }) + me() 호출, emailNeedsAgreement 가드
- handleAuthSuccess: 백엔드 user.email 우선, 없으면 provider SDK email로 fallback
- Apple: credential.email은 첫 인증 시점에만 사용 (재인증 시 자연스럽게 fallback null)

카카오 동의 항목 활성화는 카카오 개발자 콘솔 측에서 별도 진행 필요.
백엔드 측 email 채워주기(Option B)와 RootNavigator race(Option C)는 별도 트랙.
@react-native-kakao/user SDK는 useKakaoAccountLogin: true일 때만
login()에 scopes/prompts 옵션을 허용한다. 기본 카카오톡 앱 로그인 경로에서
scopes를 넘기면 Package-Assertion 에러로 로그인이 차단된다.

account_email 동의 항목은 카카오 개발자 콘솔의 동의 항목 설정으로 활성화하고,
SDK는 기본 로그인 경로 유지 + me()로 이메일 조회한다.
…lash

SignupEmail 뒤로가기 → signOut() 흐름에서 performSessionCleanup() 이
useOnboardingStore.reset() 을 먼저 호출해 onboardingStatus 가 'idle' 로 바뀌면,
authStore.sessionStatus 는 여전히 'authenticated' 인 중간 상태가 잠시 노출되어
RootNavigator condition 이 StudentNavigator 분기로 fallthrough → 학년 선택
화면이 깜빡인 뒤 AuthNavigator 로 복귀하는 race 가 발생.

signOut / runStudentVerification / verifySession catch 3곳 모두 set() 으로
sessionStatus='unauthenticated' 를 먼저 전환한 뒤 cleanup 을 수행하도록 순서를
반전. signOut 의 cleanup 도 catch 로 감싸 navigation 흐름이 막히지 않도록 한다.
/api/student/auth/quit 는 200 OK + 빈 본문(schema: content?: never)으로 응답하는데
Content-Type 이 application/json 으로 오면 openapi-fetch 가 본문을 JSON 으로
파싱하다 SyntaxError 를 던져 catch 분기로 빠진다. signOut 이 호출되지 않은 채
사용자가 재시도하면 이미 탈퇴 처리된 계정이라 AUTH_005 가 반환된다.

parseAs: 'stream' 으로 본문 파싱을 건너뛰고 error 유무로만 성공을 판단한다.
탈퇴 사유는 안내 문구상 (선택) 항목이므로 미선택 상태에서도 CTA 가
활성화되어 진행 가능해야 한다. disabled 조건과 회색 스타일을 제거한다.
…tion

WithdrawDTO.Request.reasons 에 @notempty 가 걸려 있어 빈 배열을 보내면 백엔드가
400 으로 거부한다(MAT-831). 안내 문구상 (선택) 항목이므로 사용자가 미선택 상태로
탈퇴를 진행할 수 있도록 OTHER 로 채워 보낸다.

백엔드 완화 머지/배포 후 MAT-832 에서 이 fallback 을 제거할 예정.
Replaced foreground, background, and monochrome icon images. Removed
hardcoded background color from the config.
- Updated login screen text
- Swapped Google and Kakao button styles and order
- Moved Apple login button to the bottom for iOS
sterdsterd and others added 27 commits May 27, 2026 02:01
문제 세트가 비었을 때 '풀이할 문제가 보이지 않나요? 피드백 보내기로 요청해주세요!'
안내와 함께 Feedback 화면으로 진입하는 인라인 링크를 노출한다.
홈에서 진입할 때 status bar 영역을 확보하도록 Feedback 라우트에 withTopInset 옵션을 추가.
시작 CTA 는 라벨 없는 상태에서 비활성 표시(primary-300, '문제 1번부터 풀기' fallback)로 변경.
…le results

- home-renderer: placeholder 세그먼트 루프를 bold/non-bold 분기로 분리해 가독성 개선 (동작 동일)
- useOsNotificationPermission: AppState 빠른 토글 시 in-flight 응답의 stale 덮어쓰기와 언마운트 이후 setState 를 request id + mounted ref 로 차단
[feat/MAT-873] <홈> Empty State (개발)
…deeplink nav

handleNotificationPayload 가 markNotificationAsRead 를 await 한 뒤 deeplink 로
이동하던 구조에서, 콜드스타트 + 미인증 상태에 푸시 탭이 들어오면 다음 문제가 있었다.

- markNotificationAsRead 가 authMiddleware 를 거치는 client.POST 로 나가면서
  reissue 실패 -> signOut() 사이드이펙트가 트리거될 수 있었다.
- read POST + 후속 syncNotificationBadgeCount 가 navigation 을 블로킹해
  PublishScreen 진입까지 한 박자 더 걸렸다.

waitForRouteRegistered('StudentApp') 게이트를 handleNotificationPayload
진입부에 둬서 학생 세션 hydration 이 완료된 뒤에만 read 처리/딥링크가
진행되도록 하고, markNotificationAsRead 는 fire-and-forget(void) 으로
흘려보내 navigation 즉시 시작.
…ubscriber

기존에는 React Query 캐시와 OS 앱 아이콘 뱃지가 각자 다른 fetch 경로를 통해
갱신되어 (NotificationsScreen / useFcmToken / useDeepLinkHandler / useNotificationBadgeSync
가 각각 syncNotificationBadgeCount 를 직접 호출) 같은 사용자 액션에 같은
/notification/count 가 두 번 fetch 되거나, 잠시 두 값이 다른 round trip 결과를
들고 갈 위험이 있었다.

useNotificationBadgeBridge 를 도입해 /api/student/notification/count 캐시 update
이벤트만 구독하고, 그 결과를 OS 뱃지로 push 한다. 호출자들은 이제 invalidate/
setQueryData/refetch 만 신경 쓰면 되고, OS 뱃지는 캐시를 따라간다.

- useNotificationBadgeBridge: 새 훅, App.tsx 부트 1회 마운트
- useFcmToken.onMessage: syncNotificationBadgeCount -> invalidate count 캐시
- useDeepLinkHandler.markNotificationAsRead: invalidate 만 유지, sync 호출 제거
- useNotificationBadgeSync: foreground 복귀시 invalidate (refetchType: 'all')
- NotificationsScreen: read/readAll onSuccess 의 직접 sync 호출과
  handleReadAll 의 직접 setBadge/rollback 제거 (optimistic cache patch 만으로
  subscriber 가 OS 뱃지 동기화)
- notificationBadge.ts: syncNotificationBadgeCount 제거(dead code),
  setNotificationBadgeCount/clearNotificationBadgeCount 반환 타입 Promise<void> 로
  단순화 (sentinel false/boolean 충돌 제거)
readNotification onSuccess 가 invalidate 만 하던 구조라 사용자가 단건 탭 후
서버 refetch 가 돌 때까지 hasBadge 가 잠시 남았다. handleReadAll 과 동일하게
count cache -1, 리스트 isRead=true 를 즉시 패치하고 onError 에서 rollback.

OS 앱 아이콘 뱃지는 useNotificationBadgeBridge 가 count cache update 를
구독하므로 별도 처리 불필요.
서버 사이드(StudentNotificationController + NotificationFacade DEFAULT_DAY_LIMIT=7,
MAT-907) 가 list / count / APNs badge 셋을 동일 7일 윈도우로 계산한다는 것을
JSDoc 로 남긴다. 클라이언트에서 dayLimit 을 임의로 바꾸면 alarms list,
in-app tab bell, OS app icon badge 의 source 가 어긋날 수 있다.
usePostReadNotification 와 useDeepLinkHandler.markNotificationAsRead 가 각자
같은 SyntaxError swallow 워크어라운드를 들고 있어, 백엔드가 빈 body 응답을
고치면 두 군데를 같이 손봐야 했다.

postReadNotification 헬퍼로 단일화해 client.POST + SyntaxError 처리 한 곳에서
관리한다. mutation hook 은 그 헬퍼를 mutationFn 으로 받아 단순화.
여러 위치(useDeepLinkHandler / useFcmToken / useNotificationBadgeSync /
useNotificationBadgeBridge / NotificationsScreen / useIncalidateNotificationData)
에서 /api/student/notification 과 /api/student/notification/count 의 queryKey 를
각자 raw array 또는 TanstackQueryClient.queryOptions(...).queryKey 로 만들고
있었다.

queryKeys.ts 한 곳에서 path 상수와 queryKey 를 export 해 모든 호출처가 같은
source-of-truth 를 쓰도록 통일. openapi-react-query 내부 표현이 바뀌더라도
한 곳만 손보면 된다.
…cationCount

openapi-react-query useQuery 시그니처는 (method, path, init?, options?) 인데
3번째 자리에 { enabled } 가 들어가 React Query options 가 아닌 init 으로
취급되고 있었다. 결과:

- enabled: false 를 넘겨도 query 가 비활성화되지 않음
- queryKey 가 ['get', path, { enabled }] 로 오염되어 queryKeys.ts 의
  notificationCountQueryKey (['get', path, {}]) 와 형식이 어긋남

호출부가 전부 enabled 기본값 true 라 즉시 장애는 없었지만, count queryKey 를
source-of-truth 로 쓰는 이번 작업 컨텍스트에서 source 일치를 보장하도록
init 자리에 {} 를 두고 options 를 4번째 자리로 이동.
[fix/MAT-885] 비밀번호 찾기 JSON 파싱 에러 발생
- Add selectShouldSkipEmailStep selector as single read source
- Enforce Apple invariant in setProvider/setSkipEmailStep so skipEmailStep
  cannot diverge from provider === 'APPLE'
- Type step1Data.email as string | null and normalize empty/whitespace
  to null at setEmail
- Bump persist version to 2 with migration that backfills skipEmailStep
  for existing Apple sessions and normalizes persisted email
- Consume selectShouldSkipEmailStep selector in AuthNavigator, SignupEmail,
  and SignupTerms instead of recomputing skipEmailStep || provider==='APPLE'
  at each call site
- SignupEmailScreen: switch to useLayoutEffect and early-return null so the
  email input never paints for OAuth/Apple flows
- SignupTermsScreen: prompt Alert before signing out on back navigation,
  defer beforeRemove handler with requestAnimationFrame to avoid native
  swipe-back race, and gate against duplicate prompts
- EmailLoginScreen and SignupPasswordScreen: explicitly reset provider and
  skipEmailStep before persisting email so leftover Apple flags from a
  prior session cannot leak into local signup
step1Data.email is now string | null, so trim through optional chaining and
include the email key only when a non-empty value exists.
Backend returns { code, message } envelope for OAuth login errors. Capture
the code on the thrown error and map AUTH_012 (재가입 14일 제한) to a
dedicated user-facing message instead of the generic retry copy.
…pple-flow

[fix/MAT-917] Sign in with Apple 이메일 재입력 플로우 수정
@sterdsterd sterdsterd self-assigned this May 27, 2026
@claude
Copy link
Copy Markdown

claude Bot commented May 27, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@sterdsterd sterdsterd merged commit ae361ae into main May 27, 2026
4 checks passed
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