Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4739a7
Add held atomic review-media migration successor with guarded admission
twoimo Oct 12, 2026
4d4ecf6
Require durable rehearsal receipts before atomic review-media apply
twoimo Oct 13, 2026
6dc0c6a
fix(map): show live provider failures and allow explicit retry
twoimo Oct 13, 2026
073a9de
fix(web): prepare marker styles before deferred map initialization
twoimo Oct 13, 2026
80063fe
Revert "fix(web): prepare marker styles before deferred map initializ…
twoimo Oct 13, 2026
0833ed8
Preserve terminal outcomes and reconcile fixture ownership after lost…
twoimo Oct 13, 2026
f489e43
Bind review-media expiry, attempt identity, and owned cleanup evidence
twoimo Oct 13, 2026
683f897
Merge pull request #3196 from twoimo/codex/review-db-successor-20261012
twoimo Oct 13, 2026
b94996e
Merge remote-tracking branch 'origin/develop' into codex/naver-live-f…
twoimo Oct 13, 2026
ffc8efa
Recover all SDK consumers from one retry and expose admin recovery
twoimo Oct 13, 2026
7a9b19b
Bound deferred SDK loading and cancel unmounted consumers
twoimo Oct 13, 2026
9fba35f
Track owned SDK scripts across configured provider URLs
twoimo Oct 13, 2026
7d11134
Merge pull request #3198 from twoimo/codex/naver-live-failure-state-2…
twoimo Oct 13, 2026
55a47b1
Bind credential-free SQL test transport to its manifest
twoimo Oct 13, 2026
3bca782
Refresh current proof index while preserving the prior checksum snapshot
twoimo Oct 13, 2026
4ba9c6d
Merge pull request #3200 from twoimo/codex/security-fixture-url-20261013
twoimo Oct 13, 2026
d608d9f
Merge pull request #3199 from twoimo/develop
twoimo Oct 13, 2026
71dcfc9
Bind review-media apply proofs and bound preflight execution
twoimo Oct 13, 2026
33cda47
Rebind dependent rollback evidence to fresh PG17 replay
twoimo Oct 13, 2026
ac559e1
Merge pull request #3202 from twoimo/codex/admission-proof-review-202…
twoimo Oct 13, 2026
9d652ea
Reconcile protected data promotion history
twoimo Oct 13, 2026
eecea96
Merge pull request #3204 from twoimo/codex/data-history-sync-20261014
twoimo Oct 13, 2026
8fbf12a
Merge pull request #3203 from twoimo/develop
twoimo Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 348 additions & 0 deletions .github/review-media-sql-successor.v1.json

Large diffs are not rendered by default.

13 changes: 2 additions & 11 deletions apps/web/components/admin/AdminOverviewDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useNaverMaps } from "@/hooks/use-naver-maps";
import { AdminMapLoadErrorState } from "@/components/admin/admin-map-load-error-state";
import { readAdminNaverCoordinate } from "@/lib/admin/naver-coordinate";
import { REGION_MAP_CONFIG } from "@/config/maps";
import { getNaverIndividualMarkerVisual } from "@/lib/naver-map-marker-visuals";
Expand Down Expand Up @@ -882,17 +883,7 @@ function AdminNaverMapSurface({
{isMapPreparing && <AdminMapLoadingSkeleton />}

{loadError && !isMapPreparing && (
<div className="absolute inset-0 grid place-items-center bg-card/85 p-4 text-center backdrop-blur-sm">
<div className="max-w-sm rounded-2xl bg-card/95 p-4 shadow-sm">
<p className="text-sm font-bold text-foreground">
네이버 지도를 불러오지 못했습니다
</p>
<p className="mt-2 text-xs leading-5 text-muted-foreground">
{loadError.message} 지도 키가 준비되면 홈과 같은 마커와 클러스터로
자동 표시됩니다.
</p>
</div>
</div>
<AdminMapLoadErrorState onRetry={loadNaverMaps} />
)}
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions apps/web/components/admin/admin-map-load-error-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Button } from '@/components/ui/button';

export function AdminMapLoadErrorState({ onRetry }: { onRetry: () => void }) {
return (
<div className="absolute inset-0 grid place-items-center bg-card/85 p-4 text-center backdrop-blur-sm" role="alert">
<div className="max-w-sm rounded-2xl bg-card/95 p-4 shadow-sm">
<p className="text-sm font-bold text-foreground">네이버 지도를 불러오지 못했습니다</p>
<p className="mt-2 text-xs leading-5 text-muted-foreground">연결 상태를 확인한 뒤 다시 시도해 주세요.</p>
<Button className="mt-3" variant="outline" onClick={onRetry}>다시 시도</Button>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/web/components/map/NaverMapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3687,7 +3687,7 @@ const NaverMapView = memo(({

// 로딩 에러 처리
if (loadError || mapInitError) {
return <NaverMapLoadErrorState message={loadError?.message ?? mapInitError ?? "네이버 지도를 초기화하지 못했습니다."} />;
return <NaverMapLoadErrorState message={loadError?.message ?? mapInitError ?? "네이버 지도를 초기화하지 못했습니다."} onRetry={loadError ? load : undefined} />;
}

// 로딩 중
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/map/map-view-status-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export function MapViewGoogleLoadErrorState() {
return <MapStatusNotice title="해외 지도를 불러오지 못했어요" description="잠시 후 다시 확인해 주세요." />;
}

export function NaverMapLoadErrorState(_props: { message: string }) {
return <MapStatusNotice title="지도를 불러오지 못했어요" description="잠시 후 다시 확인해 주세요." />;
export function NaverMapLoadErrorState({ onRetry }: { message: string; onRetry?: () => void }) {
return <MapStatusNotice title="지도를 불러오지 못했어요" description="잠시 후 다시 확인해 주세요." onRetry={onRetry} />;
}
157 changes: 81 additions & 76 deletions apps/web/hooks/use-naver-maps.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use client';

import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useRef } from "react";
import { NAVER_MAPS_CONFIG } from "@/config/maps";
import {
isLocalNaverStyleFallbackHost,
LOCAL_NAVER_MAP_STUB_SCRIPT_URL,
resolveNaverMapScriptUrl,
rewriteNaverStyleUrlForLocalhost,
shouldUseLocalNaverMapStub,
Expand Down Expand Up @@ -106,53 +105,14 @@ function hasUsableNaverMaps() {
&& maps.Event
);
}
function loadLocalNaverStubFallback(
setIsLoaded: (value: boolean) => void,
setIsLoading: (value: boolean) => void,
setLoadError: (value: Error | null) => void,
) {
if (hasUsableNaverMaps()) {
setIsLoaded(true);
setIsLoading(false);
return;
}

const existingStub = document.querySelector('script[data-local-naver-maps="true"]');
const attachStub = (script: HTMLScriptElement) => {
script.addEventListener('load', () => {
if (hasUsableNaverMaps()) {
setIsLoaded(true);
setLoadError(null);
} else {
setLoadError(new Error('네이버 지도 API 초기화 실패'));
}
setIsLoading(false);
});
script.addEventListener('error', () => {
setLoadError(new Error('네이버 지도 API 로딩 실패 - 네트워크 오류'));
setIsLoading(false);
});
};

if (existingStub instanceof HTMLScriptElement) {
attachStub(existingStub);
return;
}

const script = document.createElement('script');
script.type = 'text/javascript';
script.dataset.localNaverMaps = 'true';
script.src = LOCAL_NAVER_MAP_STUB_SCRIPT_URL;
script.async = true;
attachStub(script);
document.head.appendChild(script);
}

const EFFECTIVE_SCRIPT_URL = resolveNaverMapScriptUrl({
clientId: NAVER_MAPS_CONFIG.clientId,
configuredScriptUrl: process.env.NEXT_PUBLIC_NAVER_MAPS_SCRIPT_URL,
useLocalStub: LOCAL_OFFLINE_MAP,
});
const SHARED_SCRIPT_SELECTOR = LOCAL_OFFLINE_MAP
? 'script[data-local-naver-maps="true"]'
: 'script[data-tzudong-naver-maps="true"], script[src*="oapi.map.naver.com"]';

export function useNaverMaps(options: UseNaverMapsOptions = {}) {
const { autoLoad = false, strategy = 'afterInteractive' } = options;
Expand All @@ -163,11 +123,46 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) {
});
const [loadError, setLoadError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const scheduledLoadRef = useRef<(() => void) | null>(null);

// A retry belongs to the shared provider, not only to the consumer that
// requested it. Capture script completion for every mounted consumer;
// remove the subscription when that consumer leaves.
useEffect(() => {
const observeScript = (event: Event) => {
const script = event.target;
if (!(script instanceof HTMLScriptElement)) return;
if (!script.matches(SHARED_SCRIPT_SELECTOR)) return;

scheduledLoadRef.current?.();
scheduledLoadRef.current = null;
setIsLoading(false);
if (event.type === 'load' && hasUsableNaverMaps()) {
installLocalNaverStyleHostFallback();
setIsLoaded(true);
setLoadError(null);
} else if (!hasUsableNaverMaps()) {
setLoadError(new Error(event.type === 'load'
? '네이버 지도 API 초기화 실패'
: '네이버 지도 API 로딩 실패 - 네트워크 오류'));
}
};
document.addEventListener('load', observeScript, true);
document.addEventListener('error', observeScript, true);
return () => {
scheduledLoadRef.current?.();
scheduledLoadRef.current = null;
document.removeEventListener('load', observeScript, true);
document.removeEventListener('error', observeScript, true);
};
}, []);

// 수동으로 스크립트 로드하는 함수
const load = useCallback(() => {
// 이미 로드되었거나 로딩 중이면 무시
if (isLoaded || isLoading) return;
if (isLoaded || isLoading || scheduledLoadRef.current) return;

setLoadError(null);

// Client ID 검증
if (!NAVER_MAPS_CONFIG.clientId && !LOCAL_OFFLINE_MAP) {
Expand All @@ -182,61 +177,52 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) {
}

// 이미 로딩 중인 스크립트가 있는지 확인
const existingScript = document.querySelector(
LOCAL_OFFLINE_MAP
? 'script[data-local-naver-maps="true"]'
: 'script[src*="oapi.map.naver.com"]'
);
const existingScript = document.querySelector(SHARED_SCRIPT_SELECTOR);

if (existingScript) {
setIsLoading(true);
existingScript.addEventListener("load", () => {
installLocalNaverStyleHostFallback();
if (hasUsableNaverMaps()) {
setIsLoaded(true);
setIsLoading(false);
} else {
setLoadError(new Error("네이버 지도 API 초기화 실패"));
setIsLoading(false);
}
});
existingScript.addEventListener("error", () => {
setLoadError(new Error("네이버 지도 API 로딩 실패 - 네트워크 오류"));
setIsLoading(false);
});
return;
}

// 스크립트 로드 실행 함수
const injectScript = () => {
// Another consumer may have completed or injected the shared SDK
// while this consumer was waiting for an idle callback.
if (hasUsableNaverMaps()) {
setIsLoaded(true);
setIsLoading(false);
return;
}
if (document.querySelector(SHARED_SCRIPT_SELECTOR)) return;
installLocalNaverStyleHostFallback();
setIsLoading(true);
const script = document.createElement("script");
script.type = "text/javascript";
script.dataset.tzudongNaverMaps = "true";
if (LOCAL_OFFLINE_MAP) script.dataset.localNaverMaps = "true";
script.src = EFFECTIVE_SCRIPT_URL;
script.async = true;

script.onload = () => {
script.onload = null;
script.onerror = null;
installLocalNaverStyleHostFallback();
if (hasUsableNaverMaps()) {
setIsLoaded(true);
setIsLoading(false);
return;
}
if (!LOCAL_OFFLINE_MAP) {
loadLocalNaverStubFallback(setIsLoaded, setIsLoading, setLoadError);
return;
}
// A failed live provider must never become a test map. Remove
// this failed owned script so a user retry can request it again.
script.remove();
setLoadError(new Error("네이버 지도 API 초기화 실패"));
setIsLoading(false);
};

script.onerror = () => {
if (!LOCAL_OFFLINE_MAP) {
loadLocalNaverStubFallback(setIsLoaded, setIsLoading, setLoadError);
return;
}
script.onload = null;
script.onerror = null;
script.remove();
setLoadError(
new Error(
"네이버 지도 API 스크립트 로드 실패 - Client ID 또는 웹 서비스 URL을 확인해주세요."
Expand All @@ -250,14 +236,31 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) {

// 전략에 따른 로드 실행
if (strategy === 'lazyOnload') {
setIsLoading(true);
let cancelled = false;
const run = () => {
scheduledLoadRef.current = null;
if (!cancelled) injectScript();
};
// 브라우저 유휴 상태일 때 로드하거나 2초 후 로드
const idleWindow = window as Window & {
requestIdleCallback?: RequestIdleCallbackLike;
cancelIdleCallback?: (handle: IdleCallbackHandle) => void;
};
if (typeof idleWindow.requestIdleCallback === 'function') {
idleWindow.requestIdleCallback(() => injectScript(), { timeout: 2000 });
let handle: IdleCallbackHandle | undefined;
scheduledLoadRef.current = () => {
cancelled = true;
if (handle !== undefined) idleWindow.cancelIdleCallback?.(handle);
};
handle = idleWindow.requestIdleCallback(run, { timeout: 2000 });
} else {
setTimeout(injectScript, 2000);
let handle: number | undefined;
scheduledLoadRef.current = () => {
cancelled = true;
if (handle !== undefined) window.clearTimeout(handle);
};
handle = window.setTimeout(run, 2000);
}
} else {
// afterInteractive: 즉시 비동기 로드
Expand All @@ -268,10 +271,12 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) {

// autoLoad가 true면 로드 시도
useEffect(() => {
if (autoLoad) {
// After a provider failure, wait for an explicit retry. A loading-state
// change must not create an unbounded automatic request loop.
if (autoLoad && !loadError) {
load();
}
}, [autoLoad, load]);
}, [autoLoad, load, loadError]);

return { isLoaded, loadError, isLoading, load };
}
Loading
Loading