feat: 공통 컴포넌트·인증·Riverpod 기초 골격 추가

- frontend: BaseButton 공통 컴포넌트, auth.store(세션 인증)·useAuth(로그인 흐름),
  라우터 가드를 auth.store 와 연동
- flutter: dio_provider(인터셉터 적용 Dio DI), features/user 수직 슬라이스
  (model·repository·provider) 예제

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 13:58:01 +09:00
parent 6306160dda
commit b35ddfc9c3
6 changed files with 174 additions and 3 deletions
+76
View File
@@ -0,0 +1,76 @@
<script setup lang="ts">
// 공통 버튼 컴포넌트 — 모든 재사용 컴포넌트의 기준 패턴
// (타입 지정 Props/Emits, scoped 스타일, rem 단위, 기본 접근성 속성)
// 사용 예: <BaseButton label="저장" variant="primary" @click="onSave" />
// Props 정의
interface Props {
// 버튼 라벨 (접근성 aria-label 로도 사용)
label: string
// 시각 변형
variant?: 'primary' | 'secondary'
// 비활성화 여부
disabled?: boolean
// 네이티브 button type
type?: 'button' | 'submit'
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
disabled: false,
type: 'button',
})
// Emits 정의
interface Emits {
// 클릭 이벤트
(e: 'click', event: MouseEvent): void
}
const emit = defineEmits<Emits>()
// 클릭 핸들러 — 비활성화 상태에서는 이벤트를 전파하지 않음
function handleClick(event: MouseEvent) {
if (props.disabled) return
emit('click', event)
}
</script>
<template>
<button
:type="type"
:class="['base-button', `base-button--${variant}`]"
:disabled="disabled"
:aria-label="label"
:aria-disabled="disabled"
@click="handleClick"
>
{{ label }}
</button>
</template>
<style scoped>
/* 모든 치수는 rem 기준 (16px = 1rem) */
.base-button {
padding: 0.5rem 1rem;
font-size: 1rem;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
.base-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.base-button--primary {
background-color: #4f46e5;
color: #ffffff;
}
.base-button--secondary {
background-color: #e5e7eb;
color: #111827;
}
</style>
+43
View File
@@ -0,0 +1,43 @@
import { ref } from 'vue'
import { useApi } from './useApi'
import { useAuthStore } from '@/stores/auth.store'
// 인증 도메인 API Composable 샘플 — 로그인/로그아웃 흐름의 시작점
// 실제 인증 엔드포인트가 추가되면 이 패턴을 그대로 사용/확장한다.
export interface LoginPayload {
email: string
password: string
}
export function useAuth() {
const api = useApi()
const authStore = useAuthStore()
const loading = ref<boolean>(false)
async function login(payload: LoginPayload): Promise<boolean> {
loading.value = true
try {
// 서버는 인증 성공 시 HttpOnly 쿠키로 토큰을 내려준다 (응답 바디에 토큰 없음)
await api.post('/auth/login', payload)
authStore.markAuthenticated()
return true
} catch {
// 에러 메시지/알림은 useApi 인터셉터에서 전역 처리됨
return false
} finally {
loading.value = false
}
}
async function logout(): Promise<void> {
loading.value = true
try {
await api.post('/auth/logout')
} finally {
authStore.clearAuth()
loading.value = false
}
}
return { loading, login, logout }
}
+7 -3
View File
@@ -1,4 +1,5 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth.store'
// 코드 스플리팅 — 모든 페이지 컴포넌트는 lazy import 처리
const routes: RouteRecordRaw[] = [
@@ -20,10 +21,13 @@ const router = createRouter({
routes,
})
// 네비게이션 가드 — 인증 분기 (스토어 도입 후 useAuthStore로 교체)
// 네비게이션 가드 — 보호 라우트(requiresAuth) 접근 시 인증 여부 검사
router.beforeEach((to) => {
if (to.meta.requiresAuth) {
// TODO: useAuthStore().isLoggedIn 검사 후 '/login' 으로 리다이렉트
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
// 미인증 시 홈으로 리다이렉트
// (실제 로그인 페이지 추가 시 { name: 'login' } 으로 변경)
return { name: 'home' }
}
return true
})
+24
View File
@@ -0,0 +1,24 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
// 인증 세션 상태 스토어
// 웹은 JWT 를 HttpOnly·Secure 쿠키로 보관하므로(보안 규칙), JS 에서 토큰을 직접
// 저장/조회하지 않는다. 여기서는 "현재 인증된 세션인지" 여부만 관리한다.
// (사용자 프로필 데이터는 user.store.ts 에서 별도 관리)
export const useAuthStore = defineStore('auth', () => {
// 로그인 성공 또는 세션 확인 API 응답으로 갱신되는 인증 여부
const authenticated = ref<boolean>(false)
const isLoggedIn = computed<boolean>(() => authenticated.value)
// 로그인 성공 시 호출 (실제 로그인 API 연동 시 useAuth 에서 사용)
function markAuthenticated() {
authenticated.value = true
}
// 로그아웃/세션 만료 시 호출
function clearAuth() {
authenticated.value = false
}
return { isLoggedIn, markAuthenticated, clearAuth }
})