From 212f7a73eca246f389e33aa22cbf8e9e28060165 Mon Sep 17 00:00:00 2001 From: DoubtAgain Date: Mon, 11 May 2026 15:18:14 +0900 Subject: [PATCH 1/2] chore: address PR #168 CodeRabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev → main 통합 PR(#168) CodeRabbit 리뷰의 actionable 항목 일괄 반영. 주요 변경 - shared/api/http-client.ts: 수동 편집된 baseUrl 하드코딩 제거 (생성 파일 원복). 실제 baseUrl 은 httpConfig 의 createApiConfig() 가 환경 기반으로 주입. - lib/webview/bridge.ts: postBridgeMessage / navigateNative 가 boolean 반환. /mpa/home 의 navigate 호출에 router.push fallback 추가. - features/portal-link/hooks/usePortalLinkJobPolling.ts: setIsTimedOut 을 refetchInterval 콜백 밖(useEffect+setTimeout)으로 이동. isLoading / isError / error 도 함께 노출. - app/(funnel)/scraping/page.tsx: handledRef 가드로 중복 setStudentInfo/router.push 차단. - app/resync/scraping/page.tsx, app/(mpa)/mpa/resync/scraping/page.tsx: sessionStorage 동기 초기화로 인한 SSR/CSR hydration mismatch 해소 (useEffect 로 옮기고 isJobIdResolved 가드 추가). - app/api/session/refresh/route.ts: 10s AbortController 타임아웃 추가, 504 응답 처리. - app/auth/callback/route.ts: isPortalLinked 검증을 typeof === 'boolean' 으로 강화. - app/auth/success/page.tsx: Tailwind 클래스 → SCSS 모듈로 전환, 로딩/에러 UX 추가. - app/(funnel)/portal-login/components/PortalLoginForm: job_id 누락 케이스에서도 onError 호출, catch param 을 unknown 으로 좁힘. - app/(funnel)/contexts: 중복 hook useStudentInfo 제거하고 useFunnelContext 로 통합. - features/dashboard/components/SyncUpdateButton: lastSyncedAt 없을 때 '정보 업데이트' fallback. - features/portal-link/hooks/usePortalLinkSummary: jobId! non-null assertion 제거, 명시적 가드. - app/terms/page.tsx: 상대경로 SCSS 임포트를 @/app/... alias 로 변경. - docs/mpa-school-link-handoff.md: 시퀀스 다이어그램의 isPortalLinked 표기를 실제 계약과 일치 (서버 강제 false). fenced code block 에 language tag 추가. - package.json: wrangler ^4 → ^4.59.1 (CVE), @opennextjs/cloudflare ^1 → ^1.19.8. - wrangler.jsonc: compatibility_date 2024-09-23 → 2026-05-10. 검증 - yarn type-check: PASS - yarn lint: 0 errors (warnings 는 기존 잔여) - yarn build: 24 pages 정상 생성 스킵 - swagger/openapi-dev.json security 상속: 백엔드(Spring Boot)에서 fetch 되므로 로컬 패치는 다음 api:fetch 시 유실. @SecurityRequirement 어노테이션 단으로 이관 필요. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/mpa-school-link-handoff.md | 9 +- package.json | 4 +- src/app/(funnel)/complete/page.tsx | 4 +- src/app/(funnel)/contexts/FunnelContext.tsx | 8 - src/app/(funnel)/contexts/index.ts | 2 +- .../PortalLoginForm/PortalLoginForm.tsx | 11 +- src/app/(funnel)/scraping/page.tsx | 7 +- src/app/(mpa)/mpa/home/page.tsx | 14 +- src/app/(mpa)/mpa/resync/scraping/page.tsx | 15 +- src/app/api/session/refresh/route.ts | 14 +- src/app/auth/callback/route.ts | 2 +- src/app/auth/success/page.tsx | 25 +++- src/app/auth/success/success.module.scss | 38 +++++ src/app/resync/scraping/page.tsx | 15 +- src/app/terms/page.tsx | 2 +- .../SyncUpdateButton/SyncUpdateButton.tsx | 2 +- .../hooks/usePortalLinkJobPolling.ts | 24 ++- .../portal-link/hooks/usePortalLinkSummary.ts | 7 +- src/lib/webview/bridge.ts | 12 +- src/shared/api/http-client.ts | 2 +- wrangler.jsonc | 2 +- yarn.lock | 140 +++++++++--------- 22 files changed, 233 insertions(+), 126 deletions(-) create mode 100644 src/app/auth/success/success.module.scss diff --git a/docs/mpa-school-link-handoff.md b/docs/mpa-school-link-handoff.md index dbf7b7e8..c5bf815e 100644 --- a/docs/mpa-school-link-handoff.md +++ b/docs/mpa-school-link-handoff.md @@ -11,11 +11,12 @@ ## 호출 시퀀스 (목표 상태) -``` +```text 앱 (ac/re 보유, cchaksa_session 없음) │ ├─ ① POST /api/session - │ body: { accessToken, refreshToken, isPortalLinked } + │ body: { accessToken, refreshToken } + │ (isPortalLinked 는 서버가 항상 false 로 강제. 요청 바디에서 받지 않음) │ └─ BFF: sealData → Set-Cookie: cchaksa_session │ ├─ ② WebView open /mpa/resync/login @@ -83,6 +84,7 @@ Auth0 의 *Native to Web SSO* 가 사용하는 패턴. 앱이 ac/re 토큰 자 iOS 14+ 의 ITP(Intelligent Tracking Prevention) 가 app-bound 로 등록되지 않은 도메인의 쿠키를 강등시킨다. 우리가 발급하는 `cchaksa_session` 의 30일 maxAge 가 사실상 무력화되어 webview 진입마다 재익스체인지가 필요해진다. `Info.plist`: + ```xml WKAppBoundDomains @@ -126,7 +128,8 @@ iOS 14+ 의 ITP(Intelligent Tracking Prevention) 가 app-bound 로 등록되지 - 이후 앱의 ac/re 토큰이 회전될 때마다 다시 호출 (선택, webview 가 살아있는 동안 401 방지) 호출 예시: -``` + +```http POST https://cchaksa.com/api/session Content-Type: application/json diff --git a/package.json b/package.json index c3e9a722..d7501475 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "zod": "^3.24.4" }, "devDependencies": { - "@opennextjs/cloudflare": "^1", + "@opennextjs/cloudflare": "^1.19.8", "@svgr/webpack": "^8.1.0", "@tanstack/eslint-plugin-query": "^5.74.7", "@trivago/prettier-plugin-sort-imports": "^5.2.2", @@ -80,7 +80,7 @@ "swagger-typescript-api": "^13.1.1", "typescript": "5.6.3", "vitest": "^3.0.5", - "wrangler": "^4" + "wrangler": "^4.59.1" }, "packageManager": "yarn@4.5.0", "resolutions": { diff --git a/src/app/(funnel)/complete/page.tsx b/src/app/(funnel)/complete/page.tsx index 5997ec46..c75edc0c 100644 --- a/src/app/(funnel)/complete/page.tsx +++ b/src/app/(funnel)/complete/page.tsx @@ -6,7 +6,7 @@ import { ROUTES } from '@/constants/routes'; import { useInternalRouter } from '@/hooks/useInternalRouter'; import { getSemesterInfo } from '@/lib/utils/semester'; import { FunnelHeadline } from '../components'; -import { useStudentInfo } from '../contexts'; +import { useFunnelContext } from '../contexts'; import styles from './page.module.scss'; function InfoRow({ label, value }: { label: string; value: string }) { @@ -19,7 +19,7 @@ function InfoRow({ label, value }: { label: string; value: string }) { } export default function Complete() { - const { studentInfo } = useStudentInfo(); + const { studentInfo } = useFunnelContext(); const router = useInternalRouter(); const handleNext = () => { router.push(`${ROUTES.FUNNEL.TARGET_SCORE}`); diff --git a/src/app/(funnel)/contexts/FunnelContext.tsx b/src/app/(funnel)/contexts/FunnelContext.tsx index ba3288e8..363a4bfa 100644 --- a/src/app/(funnel)/contexts/FunnelContext.tsx +++ b/src/app/(funnel)/contexts/FunnelContext.tsx @@ -24,14 +24,6 @@ export function FunnelProvider({ children }: { children: ReactNode }) { ); } -export function useStudentInfo() { - const context = useContext(FunnelContext); - if (!context) { - throw new Error('useStudentInfo must be used within a FunnelProvider'); - } - return context; -} - export function useFunnelContext() { const context = useContext(FunnelContext); if (!context) { diff --git a/src/app/(funnel)/contexts/index.ts b/src/app/(funnel)/contexts/index.ts index ae0b9ad9..f8f008f4 100644 --- a/src/app/(funnel)/contexts/index.ts +++ b/src/app/(funnel)/contexts/index.ts @@ -1 +1 @@ -export { FunnelProvider, useStudentInfo, useFunnelContext } from './FunnelContext'; +export { FunnelProvider, useFunnelContext } from './FunnelContext'; diff --git a/src/app/(funnel)/portal-login/components/PortalLoginForm/PortalLoginForm.tsx b/src/app/(funnel)/portal-login/components/PortalLoginForm/PortalLoginForm.tsx index d40f45d0..b2cc9ab3 100644 --- a/src/app/(funnel)/portal-login/components/PortalLoginForm/PortalLoginForm.tsx +++ b/src/app/(funnel)/portal-login/components/PortalLoginForm/PortalLoginForm.tsx @@ -34,13 +34,16 @@ export function PortalLoginForm({ onSuccess, onError }: PortalLoginFormProps) { setJobId(newJobId); onSuccess(); } else { - setErrorMessage('연동 요청에 실패했습니다. 다시 시도해주세요.'); + const fallbackError = new Error('연동 요청에 실패했습니다. 다시 시도해주세요.'); + setErrorMessage(fallbackError.message); + onError?.(fallbackError); } - } catch (err: any) { + } catch (err: unknown) { console.error('[PortalLoginForm] 연동 요청 에러', err); - const message = err?.message ?? '알 수 없는 오류가 발생했어요\n잠시후 다시 시도해주세요'; + const errorInstance = err instanceof Error ? err : new Error(String(err)); + const message = errorInstance.message || '알 수 없는 오류가 발생했어요\n잠시후 다시 시도해주세요'; setErrorMessage(message); - onError?.(err as Error); + onError?.(errorInstance); } }; diff --git a/src/app/(funnel)/scraping/page.tsx b/src/app/(funnel)/scraping/page.tsx index 4d569d30..8ecfb257 100644 --- a/src/app/(funnel)/scraping/page.tsx +++ b/src/app/(funnel)/scraping/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { setUser } from '@sentry/nextjs'; import { ROUTES } from '@/constants/routes'; import { useInternalRouter } from '@/hooks/useInternalRouter'; @@ -19,9 +19,14 @@ export default function ScrapingPage() { const jobDetail = jobStatusData?.data; const { data: summaryData } = usePortalLinkSummary(jobId, jobStatus); + const handledRef = useRef(false); useEffect(() => { + if (handledRef.current) { + return; + } if (summaryData?.data?.studentInfo) { + handledRef.current = true; setStudentInfo(summaryData.data.studentInfo); if (jobId) { setUser({ id: jobId }); diff --git a/src/app/(mpa)/mpa/home/page.tsx b/src/app/(mpa)/mpa/home/page.tsx index f7ece838..68b52b47 100644 --- a/src/app/(mpa)/mpa/home/page.tsx +++ b/src/app/(mpa)/mpa/home/page.tsx @@ -9,14 +9,24 @@ import { SyncUpdateButton, } from '@/features/dashboard/components'; import { useRefreshProfileOnVisible } from '@/features/dashboard/hooks/useRefreshProfileOnVisible'; +import { useInternalRouter } from '@/hooks/useInternalRouter'; import { navigateNative } from '@/lib/webview'; import AsyncBoundary from '@/shared/components/AsyncBoundary'; const MpaHome = () => { useRefreshProfileOnVisible(); + const router = useInternalRouter(); - const goGraduation = () => navigateNative(ROUTES.MPA.GRADUATION_PROGRESS); - const goResync = () => navigateNative(ROUTES.MPA.RESYNC_LOGIN); + const goGraduation = () => { + if (!navigateNative(ROUTES.MPA.GRADUATION_PROGRESS)) { + router.push(ROUTES.MPA.GRADUATION_PROGRESS); + } + }; + const goResync = () => { + if (!navigateNative(ROUTES.MPA.RESYNC_LOGIN)) { + router.push(ROUTES.MPA.RESYNC_LOGIN); + } + }; return ( <> diff --git a/src/app/(mpa)/mpa/resync/scraping/page.tsx b/src/app/(mpa)/mpa/resync/scraping/page.tsx index 970ad9d6..21b6a570 100644 --- a/src/app/(mpa)/mpa/resync/scraping/page.tsx +++ b/src/app/(mpa)/mpa/resync/scraping/page.tsx @@ -22,12 +22,13 @@ export default function MpaScrapingPage() { const [errorMessage, setErrorMessage] = useState(null); const router = useInternalRouter(); - const [jobId] = useState(() => { - if (typeof window !== 'undefined') { - return sessionStorage.getItem(RESYNC_JOB_ID_KEY); - } - return null; - }); + const [jobId, setJobId] = useState(null); + const [isJobIdResolved, setIsJobIdResolved] = useState(false); + + useEffect(() => { + setJobId(sessionStorage.getItem(RESYNC_JOB_ID_KEY)); + setIsJobIdResolved(true); + }, []); const { data: jobStatusData, isTimedOut } = usePortalLinkJobPolling(jobId); const jobStatus = jobStatusData?.data?.status; @@ -64,7 +65,7 @@ export default function MpaScrapingPage() { router.push(ROUTES.MPA.RESYNC_LOGIN); }; - if (!jobId) { + if (isJobIdResolved && !jobId) { return (
diff --git a/src/app/api/session/refresh/route.ts b/src/app/api/session/refresh/route.ts index 02d0ccad..751e0ec9 100644 --- a/src/app/api/session/refresh/route.ts +++ b/src/app/api/session/refresh/route.ts @@ -5,6 +5,8 @@ import type { RefreshTokenApiResponse } from '@/shared/api/data-contracts'; export const dynamic = 'force-dynamic'; +const REFRESH_TIMEOUT_MS = 10_000; + export async function POST() { const session = await getSession(); @@ -12,11 +14,15 @@ export async function POST() { return NextResponse.json({ error: 'NO_REFRESH_TOKEN' }, { status: 401 }); } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS); + try { const response = await fetch(`${getApiBaseUrl()}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: session.refreshToken }), + signal: controller.signal, }); if (!response.ok) { @@ -37,8 +43,14 @@ export async function POST() { return NextResponse.json({ accessToken: payload.data.accessToken }); } catch (error) { - console.error('[session/refresh] unexpected error', error); session.destroy(); + if (error instanceof DOMException && error.name === 'AbortError') { + console.error('[session/refresh] timed out after', REFRESH_TIMEOUT_MS, 'ms'); + return NextResponse.json({ error: 'REFRESH_TIMEOUT' }, { status: 504 }); + } + console.error('[session/refresh] unexpected error', error); return NextResponse.json({ error: 'REFRESH_ERROR' }, { status: 500 }); + } finally { + clearTimeout(timeoutId); } } diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts index 9e649fd5..41734811 100644 --- a/src/app/auth/callback/route.ts +++ b/src/app/auth/callback/route.ts @@ -27,7 +27,7 @@ export async function GET(request: Request) { const idToken = await getKakaoToken(code, redirectUri); const { accessToken, refreshToken, isPortalLinked } = await authService.login(idToken, nonce, 'KAKAO'); - if (isPortalLinked === undefined || !refreshToken) { + if (typeof isPortalLinked !== 'boolean' || !refreshToken) { throw new AuthError('User is missing or malformed.'); } diff --git a/src/app/auth/success/page.tsx b/src/app/auth/success/page.tsx index 5682fd27..9551f48e 100644 --- a/src/app/auth/success/page.tsx +++ b/src/app/auth/success/page.tsx @@ -1,12 +1,14 @@ 'use client'; -import { useEffect, Suspense } from 'react'; +import { useEffect, useState, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { type RoutePath, useInternalRouter } from '@/hooks/useInternalRouter'; +import styles from './success.module.scss'; const SuccessContent = () => { const router = useInternalRouter(); const searchParams = useSearchParams(); + const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { const redirect = (searchParams.get('redirect') as RoutePath) || '/main'; @@ -15,25 +17,38 @@ const SuccessContent = () => { try { const response = await fetch('/api/session', { credentials: 'include' }); if (!response.ok) { - router.replace('/'); + setErrorMessage('세션을 확인할 수 없어요\n다시 로그인해주세요'); + setTimeout(() => router.replace('/'), 1500); return; } router.replace(redirect); } catch (error) { console.error('[auth/success] session hydrate failed', error); - router.replace('/'); + setErrorMessage('로그인 처리 중 오류가 발생했어요\n잠시 후 다시 시도해주세요'); + setTimeout(() => router.replace('/'), 1500); } }; hydrate(); }, [router, searchParams]); - return
; + return ( +
+ {errorMessage ? ( +

{errorMessage}

+ ) : ( + <> +
+

로그인 처리 중...

+ + )} +
+ ); }; const SuccessPage = () => { return ( -
}> + }> ); diff --git a/src/app/auth/success/success.module.scss b/src/app/auth/success/success.module.scss new file mode 100644 index 00000000..fccebef9 --- /dev/null +++ b/src/app/auth/success/success.module.scss @@ -0,0 +1,38 @@ +@use '@/styles/color.scss' as colors; +@use '@/styles/typography.scss' as typography; + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + height: 100vh; +} + +.message { + @include typography.body-md; + color: colors.$gray-700; +} + +.error { + @include typography.body-md; + color: colors.$error; + text-align: center; + white-space: pre-line; +} + +.spinner { + width: 36px; + height: 36px; + border: 3px solid colors.$gray-200; + border-top-color: colors.$purple-100; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/app/resync/scraping/page.tsx b/src/app/resync/scraping/page.tsx index 1d6566ed..b93fe836 100644 --- a/src/app/resync/scraping/page.tsx +++ b/src/app/resync/scraping/page.tsx @@ -11,12 +11,13 @@ export default function ScrapingPage() { const [errorMessage, setErrorMessage] = useState(null); const router = useInternalRouter(); - const [jobId] = useState(() => { - if (typeof window !== 'undefined') { - return sessionStorage.getItem(RESYNC_JOB_ID_KEY); - } - return null; - }); + const [jobId, setJobId] = useState(null); + const [isJobIdResolved, setIsJobIdResolved] = useState(false); + + useEffect(() => { + setJobId(sessionStorage.getItem(RESYNC_JOB_ID_KEY)); + setIsJobIdResolved(true); + }, []); const { data: jobStatusData, isTimedOut } = usePortalLinkJobPolling(jobId); const jobStatus = jobStatusData?.data?.status; @@ -48,7 +49,7 @@ export default function ScrapingPage() { throw new Error(errorMessage); } - if (!jobId) { + if (isJobIdResolved && !jobId) { throw new Error('연동 정보를 찾을 수 없습니다. 다시 로그인해주세요.'); } diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index 5d04119c..c552f1b4 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -1,4 +1,4 @@ -import styles from '../privacy-policy/PrivacyPolicy.module.scss'; +import styles from '@/app/privacy-policy/PrivacyPolicy.module.scss'; export default function TermsPage() { return ( diff --git a/src/features/dashboard/components/SyncUpdateButton/SyncUpdateButton.tsx b/src/features/dashboard/components/SyncUpdateButton/SyncUpdateButton.tsx index a208c20a..1078ebef 100644 --- a/src/features/dashboard/components/SyncUpdateButton/SyncUpdateButton.tsx +++ b/src/features/dashboard/components/SyncUpdateButton/SyncUpdateButton.tsx @@ -31,7 +31,7 @@ const SyncUpdateButton = ({ onNavigate }: SyncUpdateButtonProps = {}) => { return ( ); diff --git a/src/features/portal-link/hooks/usePortalLinkJobPolling.ts b/src/features/portal-link/hooks/usePortalLinkJobPolling.ts index fad59b71..6906b580 100644 --- a/src/features/portal-link/hooks/usePortalLinkJobPolling.ts +++ b/src/features/portal-link/hooks/usePortalLinkJobPolling.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { ENV } from '@/config/environment'; import { getJobStatus } from '../services/portalLinkService'; @@ -6,12 +6,19 @@ import { getJobStatus } from '../services/portalLinkService'; const POLLING_INTERVAL_MS = 2000; export function usePortalLinkJobPolling(jobId: string | null) { - const startedAtRef = useRef(null); const [isTimedOut, setIsTimedOut] = useState(false); useEffect(() => { setIsTimedOut(false); - startedAtRef.current = jobId ? Date.now() : null; + if (!jobId) { + return; + } + + const timeoutId = setTimeout(() => { + setIsTimedOut(true); + }, ENV.PORTAL_LINK_TIMEOUT_MS); + + return () => clearTimeout(timeoutId); }, [jobId]); const query = useQuery({ @@ -23,13 +30,18 @@ export function usePortalLinkJobPolling(jobId: string | null) { if (status === 'succeeded' || status === 'failed') { return false; } - if (startedAtRef.current && Date.now() - startedAtRef.current >= ENV.PORTAL_LINK_TIMEOUT_MS) { - setIsTimedOut(true); + if (isTimedOut) { return false; } return POLLING_INTERVAL_MS; }, }); - return { data: query.data, isTimedOut }; + return { + data: query.data, + isTimedOut, + isLoading: query.isLoading, + isError: query.isError, + error: query.error, + }; } diff --git a/src/features/portal-link/hooks/usePortalLinkSummary.ts b/src/features/portal-link/hooks/usePortalLinkSummary.ts index ae27ce34..35d9156d 100644 --- a/src/features/portal-link/hooks/usePortalLinkSummary.ts +++ b/src/features/portal-link/hooks/usePortalLinkSummary.ts @@ -4,7 +4,12 @@ import { getJobSummary } from '../services/portalLinkService'; export function usePortalLinkSummary(jobId: string | null, jobStatus: string | undefined) { return useQuery({ queryKey: ['portal-link-summary', jobId], - queryFn: () => getJobSummary(jobId!), + queryFn: () => { + if (!jobId) { + throw new Error('jobId is required to fetch portal link summary'); + } + return getJobSummary(jobId); + }, enabled: Boolean(jobId) && jobStatus === 'succeeded', }); } diff --git a/src/lib/webview/bridge.ts b/src/lib/webview/bridge.ts index 940fa90e..3b2339b5 100644 --- a/src/lib/webview/bridge.ts +++ b/src/lib/webview/bridge.ts @@ -36,10 +36,10 @@ export const isInWebView = (): boolean => { return resolveBridgePostMessage(w) !== null; }; -export const postBridgeMessage = (message: string): void => { +export const postBridgeMessage = (message: string): boolean => { const w = getWebViewWindow(); if (!w) { - return; + return false; } const post = resolveBridgePostMessage(w); @@ -48,20 +48,22 @@ export const postBridgeMessage = (message: string): void => { // eslint-disable-next-line no-console console.warn('[bridge] no native bridge detected; message dropped:', message); } - return; + return false; } try { post(message); + return true; } catch (err) { captureException(err, { extra: { bridgeMessage: message } }); if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.warn('[bridge] postMessage threw; swallowed to protect UI:', message, err); } + return false; } }; -export const navigateNative = (url: string): void => { - postBridgeMessage(`navigate:${url}`); +export const navigateNative = (url: string): boolean => { + return postBridgeMessage(`navigate:${url}`); }; diff --git a/src/shared/api/http-client.ts b/src/shared/api/http-client.ts index 65579359..fa0db472 100644 --- a/src/shared/api/http-client.ts +++ b/src/shared/api/http-client.ts @@ -62,7 +62,7 @@ export enum ContentType { } export class HttpClient { - public baseUrl: string = "https://dev.api.cchaksa.com"; + public baseUrl: string = ""; private securityData: SecurityDataType | null = null; private securityWorker?: ApiConfig["securityWorker"]; private abortControllers = new Map(); diff --git a/wrangler.jsonc b/wrangler.jsonc index dc317c7f..a01b913f 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -2,7 +2,7 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "cchaksa", "main": ".open-next/worker.js", - "compatibility_date": "2024-09-23", + "compatibility_date": "2026-05-10", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": ".open-next/assets", diff --git a/yarn.lock b/yarn.lock index e1a3cab0..ff1827ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2292,57 +2292,57 @@ __metadata: languageName: node linkType: hard -"@cloudflare/kv-asset-handler@npm:0.4.2": - version: 0.4.2 - resolution: "@cloudflare/kv-asset-handler@npm:0.4.2" - checksum: 10c0/c8877851ce069b04d32d50a640c9c0faaab054970204f64a4111bac3dd85f177c001a0b57d32f7e65269e3896268b8f94605f31e4fa06253a6a5779587a63d17 +"@cloudflare/kv-asset-handler@npm:0.5.0": + version: 0.5.0 + resolution: "@cloudflare/kv-asset-handler@npm:0.5.0" + checksum: 10c0/46863066ff1628aa76946db6ecb5088ae64ba15d7bda7e4935bd1ab1dcf080cef9cd0920205c9508721153365ef1305984e3584e7ad2dd1c9454df984f441edb languageName: node linkType: hard -"@cloudflare/unenv-preset@npm:2.16.0": - version: 2.16.0 - resolution: "@cloudflare/unenv-preset@npm:2.16.0" +"@cloudflare/unenv-preset@npm:2.16.1": + version: 2.16.1 + resolution: "@cloudflare/unenv-preset@npm:2.16.1" peerDependencies: unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + workerd: ">1.20260305.0 <2.0.0-0" peerDependenciesMeta: workerd: optional: true - checksum: 10c0/ddb2939cd05b5a0cb17d44b98a11b5c8e24dc6fa31171e98d8f1d7ffbb9cd35fde2ad2873715dc90db63183d6768ea43256ce3e4eb8c359542a2051f2d8bd919 + checksum: 10c0/08c9bd9ef488a14fd5330eb2c0829fc221f97f12295f8263dc8e3a6816ebd1a629e7255a3619b8bb688f1f58deafe28042c4e90ea84c926438c768f235f82ad9 languageName: node linkType: hard -"@cloudflare/workerd-darwin-64@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "@cloudflare/workerd-darwin-64@npm:1.20260409.1" +"@cloudflare/workerd-darwin-64@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "@cloudflare/workerd-darwin-64@npm:1.20260507.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@cloudflare/workerd-darwin-arm64@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20260409.1" +"@cloudflare/workerd-darwin-arm64@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "@cloudflare/workerd-darwin-arm64@npm:1.20260507.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@cloudflare/workerd-linux-64@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "@cloudflare/workerd-linux-64@npm:1.20260409.1" +"@cloudflare/workerd-linux-64@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "@cloudflare/workerd-linux-64@npm:1.20260507.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@cloudflare/workerd-linux-arm64@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "@cloudflare/workerd-linux-arm64@npm:1.20260409.1" +"@cloudflare/workerd-linux-arm64@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "@cloudflare/workerd-linux-arm64@npm:1.20260507.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@cloudflare/workerd-windows-64@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "@cloudflare/workerd-windows-64@npm:1.20260409.1" +"@cloudflare/workerd-windows-64@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "@cloudflare/workerd-windows-64@npm:1.20260507.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3737,9 +3737,9 @@ __metadata: languageName: node linkType: hard -"@opennextjs/aws@npm:3.10.1": - version: 3.10.1 - resolution: "@opennextjs/aws@npm:3.10.1" +"@opennextjs/aws@npm:4.0.1": + version: 4.0.1 + resolution: "@opennextjs/aws@npm:4.0.1" dependencies: "@ast-grep/napi": "npm:^0.40.5" "@aws-sdk/client-cloudfront": "npm:3.984.0" @@ -3759,20 +3759,21 @@ __metadata: urlpattern-polyfill: "npm:^10.1.0" yaml: "npm:^2.8.1" peerDependencies: - next: ">=15.5.15 || >=16.2.3" + next: ">=15.5.16 <16 || >=16.2.5" bin: open-next: dist/index.js - checksum: 10c0/0b8fab91c7bb19d7c08e93a21b278a06caccba4a3c4ce43fcda49379820d6f9d1bcf15f68278cf5a20d9a8105efa43011be11968a92b14cd1e1d605617d319c1 + checksum: 10c0/c55240c90de294015a2c6c365f0873043129a6dab2263b46eab844c40a07c698245d078d9338ad8f3cdc95dd33308c0b53dae69fa6cc79c9b69fd157a16f0405 languageName: node linkType: hard -"@opennextjs/cloudflare@npm:^1": - version: 1.19.1 - resolution: "@opennextjs/cloudflare@npm:1.19.1" +"@opennextjs/cloudflare@npm:^1.19.8": + version: 1.19.8 + resolution: "@opennextjs/cloudflare@npm:1.19.8" dependencies: "@ast-grep/napi": "npm:^0.40.5" "@dotenvx/dotenvx": "npm:1.31.0" - "@opennextjs/aws": "npm:3.10.1" + "@opennextjs/aws": "npm:4.0.1" + ci-info: "npm:^4.2.0" cloudflare: "npm:^4.4.1" comment-json: "npm:^4.5.1" enquirer: "npm:^2.4.1" @@ -3780,11 +3781,11 @@ __metadata: ts-tqdm: "npm:^0.8.6" yargs: "npm:^18.0.0" peerDependencies: - next: ">=15.5.15 || >=16.2.3" - wrangler: ^4.65.0 + next: ">=15.5.16 <16 || >=16.2.5" + wrangler: ^4.86.0 bin: opennextjs-cloudflare: dist/cli/index.js - checksum: 10c0/b5b318ba3eed46354380c61fcbdc383ee907a84576ffbf9710cd448ae73c1ae156a870035d74eedb930709535d7b2f34fbc507bea9f6cacd2fb29cc9d9dd00fb + checksum: 10c0/a593f91d0464e94e84882c5c2f0578acac0b42997af3788e583a143fd40d51f33915bac27515caf3b77852080c751cf4693c5a4d25ae3078087aea164f18a6a9 languageName: node linkType: hard @@ -7193,7 +7194,7 @@ __metadata: resolution: "chukchuk-haksa@workspace:." dependencies: "@next/bundle-analyzer": "npm:^15.3.0" - "@opennextjs/cloudflare": "npm:^1" + "@opennextjs/cloudflare": "npm:^1.19.8" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/core": "npm:^1.30.1" "@sentry/nextjs": "npm:^8" @@ -7236,11 +7237,18 @@ __metadata: typescript: "npm:5.6.3" uuid: "npm:^11.0.3" vitest: "npm:^3.0.5" - wrangler: "npm:^4" + wrangler: "npm:^4.59.1" zod: "npm:^3.24.4" languageName: unknown linkType: soft +"ci-info@npm:^4.2.0": + version: 4.4.0 + resolution: "ci-info@npm:4.4.0" + checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a + languageName: node + linkType: hard + "citty@npm:^0.1.6": version: 0.1.6 resolution: "citty@npm:0.1.6" @@ -10464,19 +10472,19 @@ __metadata: languageName: node linkType: hard -"miniflare@npm:4.20260409.0": - version: 4.20260409.0 - resolution: "miniflare@npm:4.20260409.0" +"miniflare@npm:4.20260507.1": + version: 4.20260507.1 + resolution: "miniflare@npm:4.20260507.1" dependencies: "@cspotcode/source-map-support": "npm:0.8.1" sharp: "npm:^0.34.5" - undici: "npm:7.24.4" - workerd: "npm:1.20260409.1" + undici: "npm:7.24.8" + workerd: "npm:1.20260507.1" ws: "npm:8.18.0" youch: "npm:4.1.0-beta.10" bin: miniflare: bootstrap.js - checksum: 10c0/d53db1b87c3058637dd87e7d28ef58e6b8a0309c70c832cad274af1568c3624734408b45ec8a819282080f9b9bd65521c47b1174bdb85a766896562b0d208f16 + checksum: 10c0/b74b74a5ddd6b5bb2fda190029cd07fcf1bace3085f34a60af7f4408e0d31736a042008fd7059c9831e49bf3249b02b28d60683b3ce39c921c00fe76ccc59056 languageName: node linkType: hard @@ -13417,10 +13425,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:7.24.4": - version: 7.24.4 - resolution: "undici@npm:7.24.4" - checksum: 10c0/cb302e81fadb7f0b7946ab77595715c0961b46a025ccecae79ba599432d0bc8d1e3da4dfe7ff66bc74f115c1b8ff0f099bc4e9bf313db4562da23995872c6d17 +"undici@npm:7.24.8": + version: 7.24.8 + resolution: "undici@npm:7.24.8" + checksum: 10c0/5b3cb18b1c6ccff564c37390547b2f137c666ada5083af6d5b5671dc12f73530ae2872e16fab1e3948b46013fed7c81ed10bce23f7dbf21b73794244b70b7eea languageName: node linkType: hard @@ -13922,15 +13930,15 @@ __metadata: languageName: node linkType: hard -"workerd@npm:1.20260409.1": - version: 1.20260409.1 - resolution: "workerd@npm:1.20260409.1" +"workerd@npm:1.20260507.1": + version: 1.20260507.1 + resolution: "workerd@npm:1.20260507.1" dependencies: - "@cloudflare/workerd-darwin-64": "npm:1.20260409.1" - "@cloudflare/workerd-darwin-arm64": "npm:1.20260409.1" - "@cloudflare/workerd-linux-64": "npm:1.20260409.1" - "@cloudflare/workerd-linux-arm64": "npm:1.20260409.1" - "@cloudflare/workerd-windows-64": "npm:1.20260409.1" + "@cloudflare/workerd-darwin-64": "npm:1.20260507.1" + "@cloudflare/workerd-darwin-arm64": "npm:1.20260507.1" + "@cloudflare/workerd-linux-64": "npm:1.20260507.1" + "@cloudflare/workerd-linux-arm64": "npm:1.20260507.1" + "@cloudflare/workerd-windows-64": "npm:1.20260507.1" dependenciesMeta: "@cloudflare/workerd-darwin-64": optional: true @@ -13944,25 +13952,25 @@ __metadata: optional: true bin: workerd: bin/workerd - checksum: 10c0/8fd82ddbd71a2f20ff09d125e46f1fc1999c765aa24698975c3b5b96960d593058506a82881b14e1921b7c031eec85ca559529b15354d92fe589557d3a91e8a3 + checksum: 10c0/5b17cb73e4c4245abec64e5400d4101aaccbf50f7ae378f843ba0d56c214a891126291e35efa4ee99420f65571f16fe78b47276e0c72ab6871526d13d01bb382 languageName: node linkType: hard -"wrangler@npm:^4": - version: 4.81.1 - resolution: "wrangler@npm:4.81.1" +"wrangler@npm:^4.59.1": + version: 4.90.0 + resolution: "wrangler@npm:4.90.0" dependencies: - "@cloudflare/kv-asset-handler": "npm:0.4.2" - "@cloudflare/unenv-preset": "npm:2.16.0" + "@cloudflare/kv-asset-handler": "npm:0.5.0" + "@cloudflare/unenv-preset": "npm:2.16.1" blake3-wasm: "npm:2.1.5" esbuild: "npm:0.27.3" fsevents: "npm:~2.3.2" - miniflare: "npm:4.20260409.0" + miniflare: "npm:4.20260507.1" path-to-regexp: "npm:6.3.0" unenv: "npm:2.0.0-rc.24" - workerd: "npm:1.20260409.1" + workerd: "npm:1.20260507.1" peerDependencies: - "@cloudflare/workers-types": ^4.20260409.1 + "@cloudflare/workers-types": ^4.20260507.1 dependenciesMeta: fsevents: optional: true @@ -13972,7 +13980,7 @@ __metadata: bin: wrangler: bin/wrangler.js wrangler2: bin/wrangler.js - checksum: 10c0/c738e72b9e005d8b9c2b712231da220f1386c7401bc10efea2b73e3a35809104ebb13fb8fb548979ec26e2da3ac50e161219e4d31bd7e9c25d0b4bc5e0941716 + checksum: 10c0/c8bbab061fc51cdbd500b34aeb7a3a938984085edf7c8da0f0924ef4b7454c3e05937978c8f2a6bd426506f06dd7b09869be5735915d645f5320263f854766e3 languageName: node linkType: hard From fb2b03b49c6cc05aa6cfec429be4fde3db6a1f9a Mon Sep 17 00:00:00 2001 From: DoubtAgain Date: Mon, 11 May 2026 15:24:36 +0900 Subject: [PATCH 2/2] chore: revert overreaching CodeRabbit suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #168 리뷰 반영 중 과하게 적용된 두 항목 되돌림. - @opennextjs/cloudflare ^1.19.8 → ^1 원복. CodeRabbit 자신이 선택사항이라 표시했고, ^1 도 어차피 최신 1.x 로 해석되어 install 결과가 사실상 동일. 차이는 package.json 표기뿐. - src/app/auth/success/page.tsx: 1.5s setTimeout 후 redirect 와 스피너/에러 UI 제거. CodeRabbit 권유 범위를 넘어 기존 동작(즉시 router.replace)을 바꾸는 새 UX 였음. SCSS 모듈 전환(인라인/Tailwind 금지 가이드라인)만 유지하고, 마크업은 원래 동작과 동일하게 빈 컨테이너 한 줄로 단순화. success.module.scss 도 .container 만 남기고 사용하지 않는 클래스 제거. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- src/app/auth/success/page.tsx | 22 +++------------- src/app/auth/success/success.module.scss | 32 ------------------------ yarn.lock | 4 +-- 4 files changed, 7 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index d7501475..ea8eb487 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "zod": "^3.24.4" }, "devDependencies": { - "@opennextjs/cloudflare": "^1.19.8", + "@opennextjs/cloudflare": "^1", "@svgr/webpack": "^8.1.0", "@tanstack/eslint-plugin-query": "^5.74.7", "@trivago/prettier-plugin-sort-imports": "^5.2.2", diff --git a/src/app/auth/success/page.tsx b/src/app/auth/success/page.tsx index 9551f48e..ef57a910 100644 --- a/src/app/auth/success/page.tsx +++ b/src/app/auth/success/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, Suspense } from 'react'; +import { useEffect, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { type RoutePath, useInternalRouter } from '@/hooks/useInternalRouter'; import styles from './success.module.scss'; @@ -8,7 +8,6 @@ import styles from './success.module.scss'; const SuccessContent = () => { const router = useInternalRouter(); const searchParams = useSearchParams(); - const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { const redirect = (searchParams.get('redirect') as RoutePath) || '/main'; @@ -17,33 +16,20 @@ const SuccessContent = () => { try { const response = await fetch('/api/session', { credentials: 'include' }); if (!response.ok) { - setErrorMessage('세션을 확인할 수 없어요\n다시 로그인해주세요'); - setTimeout(() => router.replace('/'), 1500); + router.replace('/'); return; } router.replace(redirect); } catch (error) { console.error('[auth/success] session hydrate failed', error); - setErrorMessage('로그인 처리 중 오류가 발생했어요\n잠시 후 다시 시도해주세요'); - setTimeout(() => router.replace('/'), 1500); + router.replace('/'); } }; hydrate(); }, [router, searchParams]); - return ( -
- {errorMessage ? ( -

{errorMessage}

- ) : ( - <> -
-

로그인 처리 중...

- - )} -
- ); + return
; }; const SuccessPage = () => { diff --git a/src/app/auth/success/success.module.scss b/src/app/auth/success/success.module.scss index fccebef9..e34ab422 100644 --- a/src/app/auth/success/success.module.scss +++ b/src/app/auth/success/success.module.scss @@ -1,38 +1,6 @@ -@use '@/styles/color.scss' as colors; -@use '@/styles/typography.scss' as typography; - .container { display: flex; - flex-direction: column; align-items: center; justify-content: center; - gap: 16px; height: 100vh; } - -.message { - @include typography.body-md; - color: colors.$gray-700; -} - -.error { - @include typography.body-md; - color: colors.$error; - text-align: center; - white-space: pre-line; -} - -.spinner { - width: 36px; - height: 36px; - border: 3px solid colors.$gray-200; - border-top-color: colors.$purple-100; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} diff --git a/yarn.lock b/yarn.lock index ff1827ef..2c0ea787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3766,7 +3766,7 @@ __metadata: languageName: node linkType: hard -"@opennextjs/cloudflare@npm:^1.19.8": +"@opennextjs/cloudflare@npm:^1": version: 1.19.8 resolution: "@opennextjs/cloudflare@npm:1.19.8" dependencies: @@ -7194,7 +7194,7 @@ __metadata: resolution: "chukchuk-haksa@workspace:." dependencies: "@next/bundle-analyzer": "npm:^15.3.0" - "@opennextjs/cloudflare": "npm:^1.19.8" + "@opennextjs/cloudflare": "npm:^1" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/core": "npm:^1.30.1" "@sentry/nextjs": "npm:^8"