-
-
{source.source_id}
-
{source.provider}
+ {writebackSources.map((source) => {
+ const sourceWritable = isCustomerOwnedWritableSource(source);
+ const sourceSelected = selectedWritebackSource?.source_id === source.source_id;
+ return (
+
-
- {source.protocol} · {source.capabilities.join(', ')}
-
-
- ))}
+
+ {source.protocol} · {source.capabilities.join(', ')}
+
+
+ etag={source.etag ?? 'missing'} · writeback={sourceWritable ? 'eligible' : 'blocked'}
+
+
+ );
+ })}
+ {sourceLoadStatus === 'ready' && writebackSources.length === 0 && (
+
+ 연결된 CalDAV/CardDAV/WebDAV source가 없습니다.
+
+ )}
{sourceLoadStatus === 'loading' && (
CalDAV source registry 확인 중입니다.
diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx
index 0a9e922e0..237243743 100644
--- a/frontend/src/components/DashboardLayout.tsx
+++ b/frontend/src/components/DashboardLayout.tsx
@@ -298,6 +298,15 @@ export function DashboardLayout({
const activeMobileView = useMobileWorkspaceView();
const startupView = useWorkspaceStartupView();
+ useEffect(() => {
+ if (!isWorkspaceMenuOpen || typeof document === 'undefined') return undefined;
+ const previousBodyOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = previousBodyOverflow;
+ };
+ }, [isWorkspaceMenuOpen]);
+
function closeMobileWorkspaceMenu() {
const menu = document.getElementById('mobile-workspace-menu') as (HTMLElement & { hidePopover?: () => void }) | null;
menu?.hidePopover?.();
diff --git a/frontend/src/components/DataLayout.tsx b/frontend/src/components/DataLayout.tsx
index 1f36acacc..70d2d6b5a 100644
--- a/frontend/src/components/DataLayout.tsx
+++ b/frontend/src/components/DataLayout.tsx
@@ -9,12 +9,13 @@ type WebdavWritebackIntentResponse = {
source_id: string | null;
server_url: string | null;
requires_if_match: boolean;
+ if_match?: string | null;
provenance: string;
status?: string | null;
message?: string | null;
};
-type WritebackStatus = 'idle' | 'loading' | 'success' | 'no_source' | 'fetch_error' | 'auth' | 'error';
+type WritebackStatus = 'idle' | 'loading' | 'success' | 'no_source' | 'fetch_error' | 'conflict' | 'auth' | 'error';
type WebdavAccountStatus = 'loading' | 'ready' | 'error';
type UniqueThreadIntentResponse = {
@@ -76,6 +77,7 @@ export function DataLayout() {
server_url: string;
username: string;
writeback_enabled: boolean;
+ etag?: string | null;
}
interface ProjectFolder {
@@ -86,6 +88,7 @@ export function DataLayout() {
const [webdavAccounts, setWebdavAccounts] = useState([]);
const [webdavAccountStatus, setWebdavAccountStatus] = useState('loading');
+ const [selectedWebdavSourceId, setSelectedWebdavSourceId] = useState(null);
const [projectFolders, setProjectFolders] = useState([]);
const [writebackStatus, setWritebackStatus] = useState('idle');
const [writebackResult, setWritebackResult] = useState(null);
@@ -97,12 +100,14 @@ export function DataLayout() {
.then((data) => {
if (!Array.isArray(data)) throw new Error('Invalid WebDAV accounts response');
setWebdavAccounts(data);
+ setSelectedWebdavSourceId(data.find((account) => account.writeback_enabled)?.source_id ?? null);
setWebdavAccountStatus('ready');
})
.catch((error: unknown) => {
console.error('WebDAV accounts fetch error', getSafeErrorSummary(error));
setWebdavAccounts([]);
setWebdavAccountStatus('error');
+ setSelectedWebdavSourceId(null);
});
apiClient.get('/api/webdav/folders')
@@ -118,7 +123,9 @@ export function DataLayout() {
setWritebackStatus('fetch_error');
return;
}
- const targetSourceId = webdavAccounts.find(acc => acc.writeback_enabled)?.source_id;
+ const targetSourceId = webdavAccounts.find((account) => (
+ account.source_id === selectedWebdavSourceId && account.writeback_enabled
+ ))?.source_id ?? webdavAccounts.find((account) => account.writeback_enabled)?.source_id;
if (!targetSourceId) {
setWritebackStatus('no_source');
return;
@@ -133,13 +140,15 @@ export function DataLayout() {
const status = getApiErrorStatus(error);
if (status === 422) {
setWritebackStatus('no_source');
+ } else if (status === 409) {
+ setWritebackStatus('conflict');
} else if (status === 401 || status === 403) {
setWritebackStatus('auth');
} else {
setWritebackStatus('error');
}
}
- }, [webdavAccounts, webdavAccountStatus]);
+ }, [selectedWebdavSourceId, webdavAccounts, webdavAccountStatus]);
const requestUniqueThreadIntent = useCallback(async () => {
setUniqueThreadStatus('loading');
@@ -161,6 +170,9 @@ export function DataLayout() {
const isWebdavSourceLoading = webdavAccountStatus === 'loading';
const canRequestWebdavWriteback = webdavAccountStatus === 'ready';
const isUniqueThreadLoading = uniqueThreadStatus === 'loading';
+ const selectedWebdavAccount = webdavAccounts.find((account) => (
+ account.source_id === selectedWebdavSourceId && account.writeback_enabled
+ )) ?? webdavAccounts.find((account) => account.writeback_enabled) ?? null;
return (
@@ -211,12 +223,31 @@ export function DataLayout() {
- {webdavAccounts.map(acc => (
-
+ {webdavAccounts.map((account) => {
+ const accountSelected = selectedWebdavAccount?.source_id === account.source_id;
+ return (
+
+ );
+ })}
+