Skip to content

Commit 38a5c0a

Browse files
SOIVclaude
andcommitted
feat(web): Phase 1.5.5 관리자 대시보드 / 일반 설정 완료
- 개인화 설정: Settings에 '로그인 후 첫 화면' 옵션 추가 (홈/마켓플레이스, fs_startup_route 저장) 로그인 시 redirectAfterLogin 없을 경우 개인화 첫 화면으로 이동 - 관리자 PIN 관리 UI: AdminView 보안 설정 패널 — 현재/새 PIN 유효성 검증, 성공/오류 상태 - 감사 로그 진입점: AdminView master-detail 패널 시스템 도입 섹션 클릭 시 오른쪽 패널 전환, 감사 로그 10건 + 필터 탭(전체/로그인/설정/PIN) 미구현 섹션(사용자/모듈/시스템)은 준비 중 플레이스홀더 - 로드맵 Phase 1.5 진행 이력을 Phase 1.9 섹션 아래에서 Phase 1.5 섹션 아래로 이동 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d3333e2 commit 38a5c0a

5 files changed

Lines changed: 469 additions & 45 deletions

File tree

apps/web/src/main.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const SS = {
8989
const LS = {
9090
theme: "fs_theme",
9191
firstVisitShown: "fs_first_visit_shown",
92+
startupRoute: "fs_startup_route",
9293
} as const;
9394

9495
// 딥 링크: 비인증 상태에서 진입한 app route 반환
@@ -98,6 +99,17 @@ function getDeepLinkTarget(): RouteKey | null {
9899
return (appRoutes as string[]).includes(hash) ? (hash as RouteKey) : null;
99100
}
100101

102+
// 개인화: 로그인 후 첫 화면 설정
103+
type StartupRoute = "home" | "marketplace";
104+
105+
function loadStartupRoute(): StartupRoute {
106+
try {
107+
const saved = localStorage.getItem(LS.startupRoute);
108+
if (saved === "marketplace") return "marketplace";
109+
} catch { /* ignore */ }
110+
return "home";
111+
}
112+
101113
// ─── App Root ─────────────────────────────────────────────────
102114
function App({ installMode }: { installMode: InstallMode }) {
103115
const [theme, setTheme] = useState<ThemeSetting>(loadTheme);
@@ -151,6 +163,12 @@ function App({ installMode }: { installMode: InstallMode }) {
151163
try { localStorage.setItem(LS.firstVisitShown, "true"); } catch { /* ignore */ }
152164
};
153165

166+
const [startupRoute, setStartupRoute] = useState<StartupRoute>(loadStartupRoute);
167+
const onStartupRouteChange = (route: StartupRoute) => {
168+
setStartupRoute(route);
169+
try { localStorage.setItem(LS.startupRoute, route); } catch { /* ignore */ }
170+
};
171+
154172
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
155173
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
156174
const [notice, setNotice] = useState(
@@ -275,8 +293,8 @@ function App({ installMode }: { installMode: InstallMode }) {
275293
try {
276294
if (localStorage.getItem(LS.firstVisitShown) !== "true") setIsFirstVisit(true);
277295
} catch { /* ignore */ }
278-
// 딥 링크 복귀
279-
const target = redirectAfterLogin ?? "home";
296+
// 딥 링크 복귀 (없으면 개인화 첫 화면)
297+
const target = redirectAfterLogin ?? startupRoute;
280298
setRedirectAfterLogin(null);
281299
navigate(target);
282300
};
@@ -310,7 +328,7 @@ function App({ installMode }: { installMode: InstallMode }) {
310328
try {
311329
if (localStorage.getItem(LS.firstVisitShown) !== "true") setIsFirstVisit(true);
312330
} catch { /* ignore */ }
313-
const target = redirectAfterLogin ?? "home";
331+
const target = redirectAfterLogin ?? startupRoute;
314332
setRedirectAfterLogin(null);
315333
navigate(target);
316334
};
@@ -431,6 +449,8 @@ function App({ installMode }: { installMode: InstallMode }) {
431449
isAdmin={isAdmin}
432450
theme={theme}
433451
onThemeChange={handleThemeChange}
452+
initialStartupRoute={startupRoute}
453+
onStartupRouteChange={onStartupRouteChange}
434454
onClose={() => setIsSettingsOpen(false)}
435455
onToggleAdmin={() => {
436456
setIsAdmin((prev) => {

apps/web/src/styles/admin.css

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@
9696
grid-template-columns: 1.2fr 1fr;
9797
}
9898

99-
.admin-sections,
100-
.admin-audit-panel {
99+
.admin-sections {
101100
border: 1px solid var(--border);
102101
border-radius: 12px;
103102
background: var(--bg-elevated);
104103
padding: 12px;
105104
display: grid;
106105
gap: 8px;
106+
align-content: start;
107107
}
108108

109109
.admin-block-title {
@@ -228,6 +228,181 @@
228228
cursor: not-allowed;
229229
}
230230

231+
/* ── Section row 활성 상태 ───────────────────────────────────── */
232+
.admin-section-row[aria-pressed="true"] {
233+
border-color: var(--accent);
234+
background: var(--accent-subtle);
235+
box-shadow: none;
236+
transform: none;
237+
}
238+
239+
.admin-section-row[aria-pressed="true"] .admin-section-name {
240+
color: var(--accent-hover);
241+
}
242+
243+
.admin-section-row[aria-pressed="true"] .admin-section-arrow {
244+
color: var(--accent);
245+
}
246+
247+
/* ── 오른쪽 패널 ─────────────────────────────────────────────── */
248+
.admin-panel {
249+
border: 1px solid var(--border);
250+
border-radius: 12px;
251+
background: var(--bg-elevated);
252+
padding: 12px;
253+
display: grid;
254+
gap: 8px;
255+
align-content: start;
256+
}
257+
258+
.admin-panel-sub-title {
259+
margin: 0;
260+
font-size: 12px;
261+
font-weight: 700;
262+
color: var(--text-muted);
263+
text-transform: uppercase;
264+
letter-spacing: 0.07em;
265+
}
266+
267+
.admin-panel-empty {
268+
margin: 0;
269+
font-size: 13px;
270+
color: var(--text-faint);
271+
padding: 12px 0;
272+
text-align: center;
273+
}
274+
275+
/* ── PIN 변경 폼 ─────────────────────────────────────────────── */
276+
.admin-pin-form {
277+
display: grid;
278+
gap: 12px;
279+
}
280+
281+
.admin-pin-error {
282+
margin: 0;
283+
font-size: 12px;
284+
color: var(--err);
285+
}
286+
287+
.admin-pin-hint {
288+
margin: 0;
289+
font-size: 11px;
290+
color: var(--text-faint);
291+
text-align: center;
292+
}
293+
294+
/* PIN 변경 성공 */
295+
.admin-pin-success {
296+
border: 1px solid rgba(16, 185, 129, 0.3);
297+
border-radius: 10px;
298+
background: rgba(16, 185, 129, 0.07);
299+
padding: 20px 14px;
300+
text-align: center;
301+
display: grid;
302+
gap: 6px;
303+
justify-items: center;
304+
}
305+
306+
.admin-pin-success-icon {
307+
margin: 0;
308+
font-size: 28px;
309+
color: var(--ok);
310+
font-weight: 800;
311+
}
312+
313+
.admin-pin-success-title {
314+
margin: 0;
315+
font-size: 14px;
316+
font-weight: 700;
317+
color: var(--ok);
318+
}
319+
320+
.admin-pin-success-desc {
321+
margin: 0;
322+
font-size: 12px;
323+
color: var(--text-muted);
324+
line-height: 1.5;
325+
}
326+
327+
/* ── 감사 로그 필터 ──────────────────────────────────────────── */
328+
.admin-audit-filters {
329+
display: flex;
330+
flex-wrap: wrap;
331+
gap: 5px;
332+
}
333+
334+
.admin-audit-filter-btn {
335+
border: 1px solid var(--border-subtle);
336+
border-radius: 999px;
337+
background: transparent;
338+
color: var(--text-muted);
339+
font-size: 11px;
340+
font-weight: 700;
341+
padding: 4px 10px;
342+
cursor: pointer;
343+
font-family: inherit;
344+
transition: all 110ms ease;
345+
}
346+
347+
.admin-audit-filter-btn:hover {
348+
background: var(--bg-hover);
349+
color: var(--text);
350+
}
351+
352+
.admin-audit-filter-btn[aria-pressed="true"] {
353+
background: var(--accent-subtle);
354+
border-color: rgba(124, 124, 240, 0.3);
355+
color: var(--accent-hover);
356+
}
357+
358+
/* 감사 로그 항목에 타입 뱃지 */
359+
.admin-audit-item {
360+
justify-content: space-between;
361+
}
362+
363+
.admin-audit-type-badge {
364+
flex-shrink: 0;
365+
margin-left: auto;
366+
padding: 2px 7px;
367+
font-size: 10px;
368+
font-weight: 700;
369+
text-transform: uppercase;
370+
letter-spacing: 0.05em;
371+
border-radius: 999px;
372+
border: 1px solid var(--border-subtle);
373+
color: var(--text-faint);
374+
background: var(--bg-surface);
375+
}
376+
377+
/* ── 미구현 플레이스홀더 ─────────────────────────────────────── */
378+
.admin-panel-placeholder {
379+
border: 1px dashed var(--border);
380+
border-radius: 10px;
381+
padding: 28px 14px;
382+
text-align: center;
383+
display: grid;
384+
gap: 6px;
385+
justify-items: center;
386+
}
387+
388+
.admin-panel-placeholder-icon {
389+
margin: 0;
390+
font-size: 28px;
391+
}
392+
393+
.admin-panel-placeholder-title {
394+
margin: 0;
395+
font-size: 14px;
396+
font-weight: 700;
397+
color: var(--text-muted);
398+
}
399+
400+
.admin-panel-placeholder-desc {
401+
margin: 0;
402+
font-size: 12px;
403+
color: var(--text-faint);
404+
}
405+
231406
/* ── Admin PIN Modal ─────────────────────────────────────────── */
232407
.pin-overlay {
233408
position: fixed;

0 commit comments

Comments
 (0)