v2.2.16: Astra Office UI Overhaul & Operations Floor

This commit is contained in:
g1nation
2026-05-16 22:21:09 +09:00
parent 9ca95ab997
commit 961e2cb4ea
20 changed files with 2632 additions and 212 deletions
+87 -16
View File
@@ -49,28 +49,99 @@ export interface CalendarConfig {
connectedAt?: string;
}
/**
* Settings + globalState 두 곳에서 읽어 merge.
*
* • VS Code Settings (g1nation.google.*) — 사용자가 직접 편집 가능한 필드:
* clientId, clientSecret, calendarId, defaultDurationMinutes, icalUrl, daysAhead
* 이 값이 채워져 있으면 globalState 의 같은 필드보다 *우선*.
*
* • globalState (CAL_CONFIG_KEY) — 마법사가 자동 관리하는 secret / runtime 필드:
* refreshToken, accessToken, accessTokenExpiresAt, connectedAs, connectedAt, lastFetchAt
* 사용자는 settings 에서 안 보임. 마법사가 OAuth 완료 후 자동 기록.
*
* • 옛 사용자 호환: globalState 에 clientId 같은 게 남아있어도 settings 가 비면
* globalState 값으로 fallback. 명시적으로 settings 에 비워두면 globalState 도 무시.
*/
export function readCalendarConfig(context: vscode.ExtensionContext): CalendarConfig {
const raw = context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig> | undefined;
const raw = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig> | undefined) ?? {};
const s = vscode.workspace.getConfiguration('g1nation.google');
const fromSettings = <T>(key: string): T | undefined => {
const v = s.get<T>(key);
// 빈 문자열은 "미설정" 으로 취급 — 사용자가 지운 케이스.
if (typeof v === 'string' && v.trim() === '') return undefined;
return v;
};
const clientId = fromSettings<string>('clientId') ?? (typeof raw.clientId === 'string' ? raw.clientId : undefined);
const clientSecret = fromSettings<string>('clientSecret') ?? (typeof raw.clientSecret === 'string' ? raw.clientSecret : undefined);
const calendarId = fromSettings<string>('calendarId') ?? (typeof raw.calendarId === 'string' ? raw.calendarId : undefined);
const defaultDurationMinutes = fromSettings<number>('defaultEventDurationMinutes')
?? (typeof raw.defaultDurationMinutes === 'number' ? raw.defaultDurationMinutes : undefined);
const icalUrl = fromSettings<string>('icalUrl') ?? (typeof raw.icalUrl === 'string' ? raw.icalUrl : '');
const daysAhead = fromSettings<number>('icalDaysAhead') ?? (typeof raw.daysAhead === 'number' && raw.daysAhead > 0 ? raw.daysAhead : 14);
return {
icalUrl: typeof raw?.icalUrl === 'string' ? raw.icalUrl : '',
daysAhead: typeof raw?.daysAhead === 'number' && raw.daysAhead > 0 ? raw.daysAhead : 14,
lastFetchAt: typeof raw?.lastFetchAt === 'string' ? raw.lastFetchAt : undefined,
clientId: typeof raw?.clientId === 'string' ? raw.clientId : undefined,
clientSecret: typeof raw?.clientSecret === 'string' ? raw.clientSecret : undefined,
refreshToken: typeof raw?.refreshToken === 'string' ? raw.refreshToken : undefined,
calendarId: typeof raw?.calendarId === 'string' ? raw.calendarId : undefined,
defaultDurationMinutes: typeof raw?.defaultDurationMinutes === 'number' ? raw.defaultDurationMinutes : undefined,
accessToken: typeof raw?.accessToken === 'string' ? raw.accessToken : undefined,
accessTokenExpiresAt: typeof raw?.accessTokenExpiresAt === 'number' ? raw.accessTokenExpiresAt : undefined,
connectedAs: typeof raw?.connectedAs === 'string' ? raw.connectedAs : undefined,
connectedAt: typeof raw?.connectedAt === 'string' ? raw.connectedAt : undefined,
icalUrl: icalUrl ?? '',
daysAhead: daysAhead ?? 14,
lastFetchAt: typeof raw.lastFetchAt === 'string' ? raw.lastFetchAt : undefined,
clientId,
clientSecret,
refreshToken: typeof raw.refreshToken === 'string' ? raw.refreshToken : undefined,
calendarId,
defaultDurationMinutes,
accessToken: typeof raw.accessToken === 'string' ? raw.accessToken : undefined,
accessTokenExpiresAt: typeof raw.accessTokenExpiresAt === 'number' ? raw.accessTokenExpiresAt : undefined,
connectedAs: typeof raw.connectedAs === 'string' ? raw.connectedAs : undefined,
connectedAt: typeof raw.connectedAt === 'string' ? raw.connectedAt : undefined,
};
}
/**
* Patch 의 필드를 settings 또는 globalState 의 적절한 곳에 분기 저장.
* • settings 로 가는 것 (사용자 편집 가능): clientId, clientSecret, calendarId,
* defaultDurationMinutes, icalUrl, daysAhead
* • globalState 로 가는 것 (secret / runtime): refreshToken, accessToken,
* accessTokenExpiresAt, connectedAs, connectedAt, lastFetchAt
*
* 한 번에 양쪽을 patch 해도 OK — 분기 자동.
*/
export async function writeCalendarConfig(context: vscode.ExtensionContext, patch: Partial<CalendarConfig>): Promise<void> {
const cur = readCalendarConfig(context);
const next: CalendarConfig = { ...cur, ...patch };
await context.globalState.update(CAL_CONFIG_KEY, next);
// Settings (g1nation.google.*) 로 가는 필드들.
const s = vscode.workspace.getConfiguration('g1nation.google');
const settingsKeys: Array<[keyof CalendarConfig, string]> = [
['clientId', 'clientId'],
['clientSecret', 'clientSecret'],
['calendarId', 'calendarId'],
['defaultDurationMinutes', 'defaultEventDurationMinutes'],
['icalUrl', 'icalUrl'],
['daysAhead', 'icalDaysAhead'],
];
for (const [src, dst] of settingsKeys) {
if (src in patch) {
const v = (patch as any)[src];
// undefined → settings 에서 제거 (default 로 복귀). 빈 문자열도 동일 취급.
const toWrite = (v === undefined || v === '') ? undefined : v;
try { await s.update(dst, toWrite, vscode.ConfigurationTarget.Global); }
catch { /* settings 쓰기 실패 시 globalState 로 fallback (다음 read 가 globalState 봄). */
const cur = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig>) ?? {};
await context.globalState.update(CAL_CONFIG_KEY, { ...cur, [src]: v });
}
}
}
// GlobalState 로 가는 secret / runtime 필드.
const globalKeys: Array<keyof CalendarConfig> = [
'refreshToken', 'accessToken', 'accessTokenExpiresAt',
'connectedAs', 'connectedAt', 'lastFetchAt',
];
const cur = (context.globalState.get(CAL_CONFIG_KEY) as Partial<CalendarConfig>) ?? {};
const next: Partial<CalendarConfig> = { ...cur };
let dirty = false;
for (const k of globalKeys) {
if (k in patch) {
(next as any)[k] = (patch as any)[k];
dirty = true;
}
}
if (dirty) await context.globalState.update(CAL_CONFIG_KEY, next);
}
/** 회사 디렉토리 내부 캐시 파일 경로. workspace 없으면 globalStorage 로 fallback. */