first commit

This commit is contained in:
2026-06-16 17:12:08 +09:00
commit e1bc5a1dfc
257 changed files with 49823 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
<script setup lang="ts">
// 앱 공통 셸 — 상단 탑바(브랜드/네비/우측 액션) + 본문 슬롯
// 모든 인증 이후 화면이 이 레이아웃을 공유한다.
import { computed, ref } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth.store'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
// 현재 경로 기준으로 상단 네비 활성 항목을 판별
const activeNav = computed<'tasks' | 'repos'>(() =>
route.path.startsWith('/repos') ? 'repos' : 'tasks',
)
// 현재 로그인 사용자 표시(아바타 이니셜 = 이름 첫 글자)
const me = computed(() => authStore.user)
const meInitial = computed(() => me.value?.name?.charAt(0) ?? '?')
// 사용자 메뉴 토글
const menuOpen = ref(false)
// 로그아웃 → 로그인 화면으로
async function onLogout() {
menuOpen.value = false
await authStore.logout()
await router.push('/login')
}
</script>
<template>
<header class="topbar">
<RouterLink
to="/repos"
class="brand"
>
<div class="logo">
R
</div>
Relay
</RouterLink>
<nav class="topnav">
<RouterLink
to="/tasks"
:class="{ active: activeNav === 'tasks' }"
>
업무
</RouterLink>
<RouterLink
to="/repos"
:class="{ active: activeNav === 'repos' }"
>
저장소
</RouterLink>
</nav>
<div class="topbar-right">
<!-- 화면별 추가 액션(: AI 에이전트 버튼) 끼워 넣는 슬롯 -->
<slot name="actions" />
<button
class="icon-btn"
type="button"
title="검색"
aria-label="검색"
>
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle
cx="11"
cy="11"
r="7"
/>
<path d="m21 21-4-4" />
</svg>
</button>
<button
class="icon-btn"
type="button"
title="알림"
aria-label="알림"
>
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
</svg>
</button>
<div class="me-wrap">
<button
class="me"
type="button"
:title="me?.name ?? '사용자'"
:aria-label="`${me?.name ?? '사용자'} 메뉴`"
aria-haspopup="menu"
:aria-expanded="menuOpen"
@click="menuOpen = !menuOpen"
>
{{ meInitial }}
</button>
<!-- 사용자 드롭다운 -->
<div
v-if="menuOpen"
class="me-menu"
role="menu"
>
<div class="me-info">
<div class="me-name">
{{ me?.name }}
</div>
<div class="me-email">
{{ me?.email }}
</div>
</div>
<button
class="me-action"
type="button"
role="menuitem"
@click="onLogout"
>
로그아웃
</button>
</div>
<!-- 메뉴 바깥 클릭 닫기 -->
<div
v-if="menuOpen"
class="me-overlay"
@click="menuOpen = false"
/>
</div>
</div>
</header>
<slot />
</template>
<style scoped>
.topbar {
height: 3.25rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 1.75rem;
padding: 0 1.25rem;
position: sticky;
top: 0;
z-index: 50;
}
.brand {
display: flex;
align-items: center;
gap: 0.5625rem;
font-weight: 700;
font-size: 0.9375rem;
letter-spacing: -0.0125rem;
color: var(--text);
text-decoration: none;
}
.brand .logo {
width: 1.625rem;
height: 1.625rem;
border-radius: 0.4375rem;
background: linear-gradient(135deg, #5b54f0, #4338ca);
display: grid;
place-items: center;
color: #fff;
font-size: 0.9375rem;
font-weight: 800;
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.35);
}
.topnav {
display: flex;
align-items: center;
gap: 0.125rem;
}
.topnav a {
color: var(--text-2);
text-decoration: none;
font-size: 0.844rem;
font-weight: 500;
padding: 0.4375rem 0.75rem;
border-radius: var(--radius-sm);
line-height: 1;
white-space: nowrap;
}
.topnav a:hover {
background: #f1f2f4;
color: var(--text);
}
.topnav a.active {
color: var(--accent);
background: var(--accent-weak);
font-weight: 600;
}
.topbar-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.5rem;
}
.icon-btn {
width: 2rem;
height: 2rem;
border-radius: var(--radius-sm);
border: none;
background: transparent;
color: var(--text-2);
display: grid;
place-items: center;
cursor: pointer;
}
.icon-btn:hover {
background: #f1f2f4;
color: var(--text);
}
.me-wrap {
position: relative;
margin-left: 0.25rem;
}
.me {
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 0.719rem;
font-weight: 700;
color: #fff;
background: #0ea5a3;
border: none;
cursor: pointer;
padding: 0;
}
/* 드롭다운 메뉴 */
.me-menu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
z-index: 60;
min-width: 11rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
.me-info {
padding: 0.75rem 0.875rem;
border-bottom: 1px solid var(--border);
}
.me-name {
font-size: 0.844rem;
font-weight: 600;
color: var(--text);
}
.me-email {
margin-top: 0.125rem;
font-size: 0.75rem;
color: var(--text-3);
}
.me-action {
width: 100%;
text-align: left;
padding: 0.625rem 0.875rem;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.844rem;
color: var(--text);
cursor: pointer;
}
.me-action:hover {
background: #f1f2f4;
}
/* 메뉴 바깥 클릭 감지용 투명 오버레이 */
.me-overlay {
position: fixed;
inset: 0;
z-index: 55;
}
</style>