This commit is contained in:
2026-01-04 17:24:47 +09:00
parent d1db71de61
commit a87c11597a
59 changed files with 15057 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
/**
* API 호출 유틸리티 composable
*/
interface ApiOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
body?: any
query?: Record<string, any>
}
export function useApi() {
const isLoading = ref(false)
const error = ref<string | null>(null)
/**
* API 호출 래퍼
*/
async function call<T>(url: string, options: ApiOptions = {}): Promise<T> {
isLoading.value = true
error.value = null
try {
const response = await $fetch<T>(url, {
method: options.method || 'GET',
body: options.body,
query: options.query
})
return response
} catch (err: any) {
const message = err.data?.message || err.message || '요청 처리 중 오류가 발생했습니다.'
error.value = message
throw new Error(message)
} finally {
isLoading.value = false
}
}
// 편의 메서드들
const get = <T>(url: string, query?: Record<string, any>) =>
call<T>(url, { method: 'GET', query })
const post = <T>(url: string, body?: any) =>
call<T>(url, { method: 'POST', body })
const put = <T>(url: string, body?: any) =>
call<T>(url, { method: 'PUT', body })
const del = <T>(url: string) =>
call<T>(url, { method: 'DELETE' })
return {
isLoading: readonly(isLoading),
error: readonly(error),
call,
get,
post,
put,
del
}
}

View File

@@ -0,0 +1,89 @@
/**
* 인증 상태 관리 composable
*/
interface User {
employeeId: number
employeeName: string
employeeEmail: string
employeePosition: string | null
}
// 전역 상태
const currentUser = ref<User | null>(null)
const isLoading = ref(false)
export function useAuth() {
/**
* 현재 로그인 사용자 조회
*/
async function fetchCurrentUser(): Promise<User | null> {
try {
isLoading.value = true
const response = await $fetch<{ user: User | null }>('/api/auth/current-user')
currentUser.value = response.user
return response.user
} catch (error) {
currentUser.value = null
return null
} finally {
isLoading.value = false
}
}
/**
* 이메일+이름으로 로그인
*/
async function login(email: string, name: string): Promise<User> {
const response = await $fetch<{ user: User }>('/api/auth/login', {
method: 'POST',
body: { email, name }
})
currentUser.value = response.user
return response.user
}
/**
* 기존 사용자 선택 로그인
*/
async function selectUser(employeeId: number): Promise<User> {
const response = await $fetch<{ user: User }>('/api/auth/select-user', {
method: 'POST',
body: { employeeId }
})
currentUser.value = response.user
return response.user
}
/**
* 최근 로그인 사용자 목록
*/
async function getRecentUsers(): Promise<User[]> {
const response = await $fetch<{ users: User[] }>('/api/auth/recent-users')
return response.users
}
/**
* 로그아웃
*/
async function logout(): Promise<void> {
await $fetch('/api/auth/logout', { method: 'POST' })
currentUser.value = null
}
/**
* 로그인 여부 확인
*/
const isLoggedIn = computed(() => currentUser.value !== null)
return {
currentUser: readonly(currentUser),
isLoading: readonly(isLoading),
isLoggedIn,
fetchCurrentUser,
login,
selectUser,
getRecentUsers,
logout
}
}

View File

@@ -0,0 +1,119 @@
/**
* ISO 8601 주차 계산 composable
*/
interface WeekInfo {
year: number
week: number
startDate: Date
endDate: Date
startDateStr: string
endDateStr: string
weekString: string // "2026-W01" 형식
}
export function useWeekCalc() {
/**
* 특정 날짜의 ISO 주차 정보 반환
*/
function getWeekInfo(date: Date = new Date()): WeekInfo {
const target = new Date(date)
target.setHours(0, 0, 0, 0)
// 목요일 기준으로 연도 판단 (ISO 규칙)
const thursday = new Date(target)
thursday.setDate(target.getDate() - ((target.getDay() + 6) % 7) + 3)
const year = thursday.getFullYear()
const firstThursday = new Date(year, 0, 4)
firstThursday.setDate(firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3)
const week = Math.ceil((thursday.getTime() - firstThursday.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1
// 해당 주의 월요일
const monday = new Date(target)
monday.setDate(target.getDate() - ((target.getDay() + 6) % 7))
// 해당 주의 일요일
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
return {
year,
week,
startDate: monday,
endDate: sunday,
startDateStr: formatDate(monday),
endDateStr: formatDate(sunday),
weekString: `${year}-W${week.toString().padStart(2, '0')}`
}
}
/**
* 이번 주 정보
*/
function getCurrentWeekInfo(): WeekInfo {
return getWeekInfo(new Date())
}
/**
* 지난 주 정보
*/
function getLastWeekInfo(): WeekInfo {
const lastWeek = new Date()
lastWeek.setDate(lastWeek.getDate() - 7)
return getWeekInfo(lastWeek)
}
/**
* 날짜 포맷 (YYYY-MM-DD)
*/
function formatDate(date: Date): string {
return date.toISOString().split('T')[0]
}
/**
* 주차 문자열 파싱
*/
function parseWeekString(weekStr: string): { year: number; week: number } | null {
const match = weekStr.match(/^(\d{4})-W(\d{2})$/)
if (!match) return null
return { year: parseInt(match[1]), week: parseInt(match[2]) }
}
/**
* 주차별 날짜 범위 텍스트
*/
function getWeekRangeText(year: number, week: number): string {
// 해당 연도 첫 번째 목요일 찾기
const jan4 = new Date(year, 0, 4)
const firstThursday = new Date(jan4)
firstThursday.setDate(jan4.getDate() - ((jan4.getDay() + 6) % 7) + 3)
// 해당 주차의 월요일
const monday = new Date(firstThursday)
monday.setDate(firstThursday.getDate() - 3 + (week - 1) * 7)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
return `${formatDateKr(monday)} ~ ${formatDateKr(sunday)}`
}
/**
* 한국어 날짜 포맷 (M월 D일)
*/
function formatDateKr(date: Date): string {
return `${date.getMonth() + 1}${date.getDate()}`
}
return {
getWeekInfo,
getCurrentWeekInfo,
getLastWeekInfo,
formatDate,
parseWeekString,
getWeekRangeText,
formatDateKr
}
}