Phase 0: wbx-spring-core 라이브러리 전환 - java-library 플러그인, WbxAutoConfiguration, Admin 조건부 활성화 - 루트 settings.gradle + build.gradle (멀티모듈) Phase 1: wtm-api 모듈 생성 - 23개 JPA Entity, 14개 Controller, 79개 API 엔드포인트 - Flyway V100~V107 MySQL 마이그레이션 - TimesheetRuleEngine, TimesheetApprovalHandler, P6WbsParser Phase 2: wtm-frontend (Vue 3 + PrimeVue 4) - 10개 도메인 모듈, 17개 View, 5개 서브컴포넌트 - 반응형 레이아웃 (AppLayout, AppSidebar, AppTopbar) - BaseCrudTable, BaseFormDialog, BasePageHeader 표준 컴포넌트 - JWT 인터셉터, 역할 기반 메뉴 필터링 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
28 KiB
28 KiB
13. WTM Frontend 프로젝트 구성 계획
작성일: 2026-03-25 기술 스택: Vue 3.5 + TypeScript + PrimeVue 4 + Pinia + Vite 목적: 여러 개발자가 동시 개발 가능한 모듈 기반 프론트엔드 구조 설계
1. 기술 스택
| 도구 | 버전 | 용도 |
|---|---|---|
| Vite | 6.x | 빌드, HMR 개발 서버 |
| Vue | 3.5+ | Composition API + <script setup> |
| TypeScript | 5.5+ | 전체 프로젝트 타입 안전성 |
| PrimeVue | 4.x | UI 컴포넌트 (80+ 제공) |
| @primeuix/themes | latest | Aura 테마 프리셋 |
| Pinia | 2.x | 도메인별 상태 관리 |
| Vue Router | 4.x | SPA 라우팅, lazy loading |
| Axios | 1.x | HTTP 클라이언트 (JWT 인터셉터) |
| VueUse | 12.x | 유틸리티 컴포저블 |
| Chart.js | 4.x | 차트 (PrimeVue Chart 래퍼) |
| ESLint | 9.x | 린트 (flat config) |
| Prettier | 3.x | 코드 포매팅 |
| Vitest | 2.x | 단위 테스트 |
2. 프로젝트 구조
설계 원칙
- 도메인(Feature) 기반 모듈 분리: 각 개발자가 담당 모듈만 작업, 최소 충돌
- 모듈 자급자족: 각 모듈이 views/components/store/service/types/routes를 독립 보유
- core는 인프라만: 비즈니스 로직 없이 공유 인프라(API, 인증, 레이아웃)만 제공
- cross-module 의존은 core를 통해서만: 모듈 간 직접 import 금지
wtm-frontend/
├── public/
│ └── favicon.ico
│
├── src/
│ ├── app/ # 애플리케이션 셸
│ │ ├── App.vue
│ │ ├── main.ts
│ │ ├── router.ts # 루트 라우터 (모듈 라우트 조립)
│ │ └── plugins/
│ │ ├── primevue.ts # PrimeVue 설정 + 테마
│ │ ├── pinia.ts
│ │ └── index.ts
│ │
│ ├── core/ # 공유 인프라 (비즈니스 로직 없음)
│ │ ├── api/
│ │ │ ├── axios.ts # Axios 인스턴스 + JWT 인터셉터
│ │ │ ├── api.types.ts # PageResponse, ErrorResponse 등
│ │ │ └── endpoints.ts # BASE_URL 상수
│ │ ├── auth/
│ │ │ ├── auth.guard.ts # 네비게이션 가드
│ │ │ ├── auth.service.ts # 토큰 저장/갱신
│ │ │ └── auth.types.ts
│ │ ├── composables/ # 공용 컴포저블
│ │ │ ├── useAppToast.ts # Toast 래퍼
│ │ │ ├── useConfirmAction.ts # ConfirmDialog 래퍼
│ │ │ ├── usePagination.ts # DataTable 페이징 헬퍼
│ │ │ └── usePermission.ts # 역할/권한 체크
│ │ ├── components/ # 공용 레이아웃/베이스 컴포넌트
│ │ │ ├── AppLayout.vue # 사이드바 + 탑바 + 콘텐츠
│ │ │ ├── AppSidebar.vue
│ │ │ ├── AppTopbar.vue
│ │ │ ├── BaseCrudTable.vue # 재사용 DataTable + CRUD 셸
│ │ │ ├── BaseFormDialog.vue # 재사용 폼 다이얼로그
│ │ │ ├── BasePageHeader.vue
│ │ │ └── NotFoundView.vue
│ │ ├── constants/
│ │ │ └── app.constants.ts
│ │ ├── types/
│ │ │ ├── common.types.ts # 공용 타입 (ID, Timestamp 등)
│ │ │ └── menu.types.ts
│ │ └── utils/
│ │ ├── date.utils.ts
│ │ ├── format.utils.ts
│ │ └── validation.utils.ts
│ │
│ ├── modules/ # ===== 도메인 모듈 (개발자별 담당) =====
│ │ │
│ │ ├── auth/ # 👤 풀스택 ③ 담당
│ │ │ ├── views/
│ │ │ │ ├── LoginView.vue
│ │ │ │ ├── ForgotPasswordView.vue
│ │ │ │ └── ChangePasswordView.vue
│ │ │ ├── components/
│ │ │ │ └── LoginForm.vue
│ │ │ ├── auth.routes.ts
│ │ │ ├── auth.store.ts
│ │ │ ├── auth.service.ts
│ │ │ └── auth.types.ts
│ │ │
│ │ ├── dashboard/ # 👤 풀스택 ③ 담당
│ │ │ ├── views/
│ │ │ │ └── DashboardView.vue
│ │ │ ├── components/
│ │ │ │ ├── StatCard.vue
│ │ │ │ ├── TimesheetChart.vue
│ │ │ │ ├── PendingApprovalCard.vue
│ │ │ │ └── RecentActivityList.vue
│ │ │ ├── dashboard.routes.ts
│ │ │ ├── dashboard.store.ts
│ │ │ ├── dashboard.service.ts
│ │ │ └── dashboard.types.ts
│ │ │
│ │ ├── user/ # 👤 풀스택 ③ 담당
│ │ │ ├── views/
│ │ │ │ ├── UserListView.vue
│ │ │ │ └── UserDetailView.vue
│ │ │ ├── components/
│ │ │ │ ├── UserTable.vue
│ │ │ │ ├── UserFormDialog.vue
│ │ │ │ ├── UserRoleBadge.vue
│ │ │ │ └── UserExcelUpload.vue
│ │ │ ├── user.routes.ts
│ │ │ ├── user.store.ts
│ │ │ ├── user.service.ts
│ │ │ └── user.types.ts
│ │ │
│ │ ├── project/ # 👤 풀스택 ① 담당
│ │ │ ├── views/
│ │ │ │ ├── ProjectListView.vue
│ │ │ │ └── ProjectDetailView.vue
│ │ │ ├── components/
│ │ │ │ ├── ProjectTable.vue
│ │ │ │ ├── ProjectFormDialog.vue
│ │ │ │ ├── ProjectStatusTag.vue
│ │ │ │ └── ProjectMemberTable.vue
│ │ │ ├── project.routes.ts
│ │ │ ├── project.store.ts
│ │ │ ├── project.service.ts
│ │ │ └── project.types.ts
│ │ │
│ │ ├── wbs/ # 👤 풀스택 ① 담당
│ │ │ ├── views/
│ │ │ │ ├── WbsTreeView.vue
│ │ │ │ └── WbsCompareView.vue # PH1-2
│ │ │ ├── components/
│ │ │ │ ├── WbsTreeTable.vue # PrimeVue TreeTable
│ │ │ │ ├── WbsVersionSelect.vue
│ │ │ │ ├── WbsUploadDialog.vue
│ │ │ │ └── WbsComparePanel.vue # PH1-2
│ │ │ ├── wbs.routes.ts
│ │ │ ├── wbs.store.ts
│ │ │ ├── wbs.service.ts
│ │ │ └── wbs.types.ts
│ │ │
│ │ ├── teal/ # 👤 풀스택 ① 담당
│ │ │ ├── views/
│ │ │ │ └── TealListView.vue
│ │ │ ├── components/
│ │ │ │ ├── TealTable.vue
│ │ │ │ ├── TealUploadDialog.vue
│ │ │ │ └── TealVersionSelect.vue
│ │ │ ├── teal.routes.ts
│ │ │ ├── teal.store.ts
│ │ │ ├── teal.service.ts
│ │ │ └── teal.types.ts
│ │ │
│ │ ├── settings/ # 👤 풀스택 ③ 담당 (SA 전용)
│ │ │ ├── views/
│ │ │ │ └── SettingsView.vue # 탭: Overhead Types | Work Rules
│ │ │ ├── components/
│ │ │ │ ├── OverheadTypeTable.vue
│ │ │ │ ├── OverheadTypeFormDialog.vue
│ │ │ │ └── WorkRuleForm.vue
│ │ │ ├── settings.routes.ts
│ │ │ ├── settings.store.ts
│ │ │ ├── settings.service.ts # overhead-types + work-rules API
│ │ │ └── settings.types.ts
│ │ │
│ │ ├── timesheet/ # 👤 풀스택 ② 담당
│ │ │ ├── views/
│ │ │ │ ├── TimesheetWeekView.vue # 메인 — 주간 시수 입력
│ │ │ │ ├── TimesheetHistoryView.vue
│ │ │ │ └── TimesheetUploadView.vue
│ │ │ ├── components/
│ │ │ │ ├── TimesheetGrid.vue # DataTable 편집 가능 셀
│ │ │ │ ├── TimesheetEntryRow.vue # 한 행 (프로젝트+WBS+TEAL+일별시간)
│ │ │ │ ├── TimesheetTabPanel.vue # Non-Project | Other | EPC 탭
│ │ │ │ ├── TimesheetSummaryCard.vue # 주간 합계, 규칙 위반 경고
│ │ │ │ ├── TimesheetStatusBadge.vue
│ │ │ │ ├── TimesheetWeekPicker.vue # 주간 날짜 선택
│ │ │ │ └── TimesheetExcelUpload.vue
│ │ │ ├── timesheet.routes.ts
│ │ │ ├── timesheet.store.ts
│ │ │ ├── timesheet.service.ts
│ │ │ └── timesheet.types.ts
│ │ │
│ │ ├── approval/ # 👤 풀스택 ② 담당
│ │ │ ├── views/
│ │ │ │ ├── ApprovalPendingView.vue # 결재 대기 목록
│ │ │ │ └── ApprovalHistoryView.vue
│ │ │ ├── components/
│ │ │ │ ├── ApprovalTable.vue
│ │ │ │ ├── ApprovalActionBar.vue # 승인/반려/일괄승인 버튼
│ │ │ │ ├── ApprovalDetailDialog.vue # 시수 상세 + 코멘트
│ │ │ │ ├── ApprovalTimeline.vue # PrimeVue Timeline
│ │ │ │ └── ApprovalCommentForm.vue
│ │ │ ├── approval.routes.ts
│ │ │ ├── approval.store.ts
│ │ │ ├── approval.service.ts
│ │ │ └── approval.types.ts
│ │ │
│ │ └── report/ # 👤 풀스택 ① 담당
│ │ ├── views/
│ │ │ └── ReportView.vue
│ │ ├── components/
│ │ │ ├── ReportFilterPanel.vue # 기간, 프로젝트, groupBy 필터
│ │ │ ├── ProjectHoursChart.vue # Bar/Line 차트
│ │ │ ├── WbsHoursChart.vue
│ │ │ ├── ReportDataTable.vue # 집계 테이블
│ │ │ └── ReportExportButton.vue # Excel 다운로드
│ │ ├── report.routes.ts
│ │ ├── report.store.ts
│ │ ├── report.service.ts
│ │ └── report.types.ts
│ │
│ └── assets/
│ ├── styles/
│ │ ├── _variables.scss # 색상, 간격 변수
│ │ ├── _overrides.scss # PrimeVue 테마 오버라이드
│ │ └── main.scss
│ └── images/
│ └── logo.svg
│
├── .env # 기본값 (커밋)
├── .env.development # 로컬 개발 (커밋)
├── .env.production # 운영 (커밋)
├── .env.local # 개인 오버라이드 (gitignore)
├── .gitignore
├── .prettierrc.json
├── eslint.config.js
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
├── index.html
└── package.json
3. 개발자 담당 배분
| 개발자 | 담당 모듈 | 화면 수 | 비고 |
|---|---|---|---|
| 풀스택 ① | project, wbs, teal, report | 10 | WBS TreeTable, P6 업로드, 차트 |
| 풀스택 ② | timesheet, approval | 7 | 시수 입력 핵심, 결재 워크플로우 |
| 풀스택 ③ | auth, dashboard, user | 9 | 로그인/SSO, 대시보드, 인력 관리 |
| 공통 (리드) | core/, app/ | - | 레이아웃, API 인프라, 베이스 컴포넌트 |
충돌 최소화 규칙
✅ 허용: modules/timesheet/ 내 파일 자유 수정
✅ 허용: core/composables/ 에 새 컴포저블 추가
❌ 금지: 다른 모듈 직접 import (modules/approval → modules/timesheet)
❌ 금지: core/components/ 기존 파일 단독 수정 (리드 승인 필요)
⚠️ 주의: app/router.ts 는 모듈 routes만 import (자동 조립)
4. 모듈 내부 파일 규칙
모든 모듈은 동일한 6파일 패턴을 따릅니다:
modules/{module}/
├── views/ # 라우트에 매핑되는 페이지 컴포넌트
│ └── {Module}ListView.vue
├── components/ # 모듈 전용 하위 컴포넌트
│ └── {Module}Table.vue
├── {module}.routes.ts # RouteRecordRaw[] export
├── {module}.store.ts # Pinia defineStore (Composition API)
├── {module}.service.ts # Axios API 호출 함수
└── {module}.types.ts # 모듈 전용 TypeScript 타입/인터페이스
4.1 라우트 파일 (lazy loading)
// modules/timesheet/timesheet.routes.ts
import type { RouteRecordRaw } from 'vue-router';
export const timesheetRoutes: RouteRecordRaw[] = [
{
path: '/timesheets',
name: 'timesheet-week',
component: () => import('./views/TimesheetWeekView.vue'),
meta: { title: '시수 입력', icon: 'pi pi-clock', roles: ['USER', 'DL', 'PM', 'SA'] },
},
{
path: '/timesheets/history',
name: 'timesheet-history',
component: () => import('./views/TimesheetHistoryView.vue'),
meta: { title: '시수 이력', roles: ['USER', 'DL', 'PM', 'SA'] },
},
{
path: '/timesheets/upload',
name: 'timesheet-upload',
component: () => import('./views/TimesheetUploadView.vue'),
meta: { title: 'Excel 업로드', roles: ['USER'] },
},
];
4.2 서비스 파일 (API 호출)
// modules/timesheet/timesheet.service.ts
import api from '@/core/api/axios';
import type { Timesheet, TimesheetEntry, TimesheetFilter } from './timesheet.types';
import type { PageResponse } from '@/core/api/api.types';
const BASE = '/api/wtm/timesheets';
export const timesheetService = {
getWeekly: (weekStart: string) =>
api.get<Timesheet>(`${BASE}/week`, { params: { weekStart } }),
saveEntry: (timesheetId: number, entry: Partial<TimesheetEntry>) =>
api.post<TimesheetEntry>(`${BASE}/${timesheetId}/entries`, entry),
saveBatch: (timesheetId: number, entries: Partial<TimesheetEntry>[]) =>
api.put<Timesheet>(`${BASE}/${timesheetId}/entries/batch`, entries),
deleteEntry: (timesheetId: number, entryId: number) =>
api.delete<void>(`${BASE}/${timesheetId}/entries/${entryId}`),
submit: (timesheetId: number) =>
api.post<Timesheet>(`${BASE}/${timesheetId}/submit`),
getHistory: (params: TimesheetFilter) =>
api.get<PageResponse<Timesheet>>(`${BASE}/history`, { params }),
uploadExcel: (file: File, weekStart: string) => {
const form = new FormData();
form.append('file', file);
form.append('weekStart', weekStart);
return api.post(`${BASE}/upload`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
},
downloadTemplate: () =>
api.get(`${BASE}/upload/template`, { responseType: 'blob' }),
};
4.3 스토어 파일 (Pinia Composition API)
// modules/timesheet/timesheet.store.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { timesheetService } from './timesheet.service';
import type { Timesheet, TimesheetEntry } from './timesheet.types';
export const useTimesheetStore = defineStore('timesheet', () => {
// State
const current = ref<Timesheet | null>(null);
const loading = ref(false);
const saving = ref(false);
// Getters
const totalHours = computed(() =>
current.value?.entries.reduce((sum, e) => sum + e.hours, 0) ?? 0,
);
const isOverWeeklyLimit = computed(() => totalHours.value > 52);
const canSubmit = computed(() =>
current.value?.status === 'DRAFT' || current.value?.status === 'REJECTED',
);
// Actions
async function fetchWeekly(weekStart: string) {
loading.value = true;
try {
const { data } = await timesheetService.getWeekly(weekStart);
current.value = data;
} finally {
loading.value = false;
}
}
async function saveEntry(entry: Partial<TimesheetEntry>) {
if (!current.value) return;
saving.value = true;
try {
await timesheetService.saveEntry(current.value.id, entry);
await fetchWeekly(current.value.weekStartDate);
} finally {
saving.value = false;
}
}
async function submit() {
if (!current.value) return;
await timesheetService.submit(current.value.id);
await fetchWeekly(current.value.weekStartDate);
}
function $reset() {
current.value = null;
loading.value = false;
saving.value = false;
}
return {
current, loading, saving,
totalHours, isOverWeeklyLimit, canSubmit,
fetchWeekly, saveEntry, submit, $reset,
};
});
4.4 타입 파일
// modules/timesheet/timesheet.types.ts
export type TimesheetStatus = 'DRAFT' | 'SUBMITTED' | 'DL_APPROVED' | 'APPROVED' | 'REJECTED';
export type EntryType = 'NON_PROJECT' | 'OTHER_PROJECT' | 'EPC';
export interface Timesheet {
id: number;
userId: number;
weekStartDate: string; // ISO date
weekEndDate: string;
status: TimesheetStatus;
totalHours: number;
entries: TimesheetEntry[];
submittedAt: string | null;
}
export interface TimesheetEntry {
id: number;
entryType: EntryType;
entryDate: string;
hours: number;
npCategory?: string;
otherProjectId?: number;
otherCategory?: string;
epcProjectId?: number;
canonicalWbsId?: number;
tealEntryId?: number;
revisionNumber?: number;
remark?: string;
}
export interface TimesheetFilter {
from?: string;
to?: string;
status?: TimesheetStatus;
skip?: number;
limit?: number;
}
5. 공용 API 인프라 (core/api)
5.1 Axios 인스턴스 + JWT 인터셉터
// core/api/axios.ts
import axios from 'axios';
import type { InternalAxiosRequestConfig, AxiosError } from 'axios';
import { authService } from '@/core/auth/auth.service';
import router from '@/app/router';
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000,
headers: { 'Content-Type': 'application/json' },
});
// Request: JWT 토큰 첨부
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = authService.getAccessToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response: 401 시 토큰 갱신 + 재시도
let isRefreshing = false;
let failedQueue: Array<{ resolve: Function; reject: Function }> = [];
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status === 401 && !original._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string) => {
original.headers.Authorization = `Bearer ${token}`;
resolve(api(original));
},
reject,
});
});
}
original._retry = true;
isRefreshing = true;
try {
const newToken = await authService.refreshToken();
failedQueue.forEach((q) => q.resolve(newToken));
failedQueue = [];
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
} catch {
failedQueue.forEach((q) => q.reject(error));
failedQueue = [];
authService.clearTokens();
router.push({ name: 'login' });
return Promise.reject(error);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
},
);
export default api;
5.2 공용 응답 타입
// core/api/api.types.ts
/** WBX 호환 목록 응답 (items + total) */
export interface PageResponse<T> {
items: T[];
total: number;
skip: number;
limit: number;
}
/** WBX 호환 에러 응답 (detail 키) */
export interface ErrorResponse {
detail: string;
code?: string;
}
6. 라우터 조립 (app/router.ts)
// app/router.ts
import { createRouter, createWebHistory } from 'vue-router';
import { authGuard } from '@/core/auth/auth.guard';
import { authRoutes } from '@/modules/auth/auth.routes';
import { dashboardRoutes } from '@/modules/dashboard/dashboard.routes';
import { userRoutes } from '@/modules/user/user.routes';
import { projectRoutes } from '@/modules/project/project.routes';
import { wbsRoutes } from '@/modules/wbs/wbs.routes';
import { tealRoutes } from '@/modules/teal/teal.routes';
import { timesheetRoutes } from '@/modules/timesheet/timesheet.routes';
import { approvalRoutes } from '@/modules/approval/approval.routes';
import { reportRoutes } from '@/modules/report/report.routes';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// 인증 불필요 (레이아웃 없음)
...authRoutes,
// 인증 필요 (AppLayout 래핑)
{
path: '/',
component: () => import('@/core/components/AppLayout.vue'),
beforeEnter: authGuard,
redirect: '/dashboard',
children: [
...dashboardRoutes,
...userRoutes,
...projectRoutes,
...wbsRoutes,
...tealRoutes,
...timesheetRoutes,
...approvalRoutes,
...reportRoutes,
],
},
// 404
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/core/components/NotFoundView.vue'),
},
],
});
export default router;
7. PrimeVue 컴포넌트 매핑 (WTM 기능별)
| WTM 기능 | PrimeVue 컴포넌트 | 사용 위치 |
|---|---|---|
| 사용자/프로젝트 CRUD | DataTable, Column, Dialog |
UserTable, ProjectTable |
| WBS 트리 표시 | TreeTable, Tree |
WbsTreeTable |
| 시수 입력 (주간 그리드) | DataTable (editable cells), InputNumber |
TimesheetGrid |
| 시수 유형 탭 | Tabs, TabList, Tab, TabPanels, TabPanel |
TimesheetTabPanel |
| 결재 이력 | Timeline |
ApprovalTimeline |
| 대시보드 차트 | Chart (Chart.js 래퍼) |
TimesheetChart, ProjectHoursChart |
| 파일 업로드 | FileUpload |
WbsUploadDialog, TimesheetExcelUpload |
| 알림/토스트 | Toast |
전역 (useAppToast) |
| 삭제 확인 | ConfirmDialog |
전역 (useConfirmAction) |
| 상태 표시 | Tag, Badge |
TimesheetStatusBadge, ProjectStatusTag |
| 날짜 선택 | DatePicker |
TimesheetWeekPicker, ReportFilterPanel |
| 드롭다운 | Select, MultiSelect, AutoComplete |
프로젝트/WBS/TEAL 선택 |
| 폼 입력 | InputText, InputNumber, Textarea |
모든 FormDialog |
| 사이드바 메뉴 | PanelMenu |
AppSidebar |
| 상단바 | Menubar, Avatar |
AppTopbar |
| 로딩 상태 | Skeleton, ProgressBar |
DataTable loading |
| 페이지 레이아웃 | Card, Panel, Divider |
전체 |
8. 환경 변수
# .env (기본값, 커밋)
VITE_APP_TITLE=WTM - Work Time Manager
# .env.development (로컬 개발, 커밋)
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_ENV=development
# .env.production (운영, 커밋)
VITE_API_BASE_URL=https://wtmgr.hanwhaocean.com
VITE_APP_ENV=production
# .env.local (개인 오버라이드, gitignore)
VITE_API_BASE_URL=http://192.168.x.x:8080
// src/env.d.ts — 타입 안전 접근
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_APP_TITLE: string;
readonly VITE_APP_ENV: 'development' | 'staging' | 'production';
}
9. ESLint + Prettier 설정
.prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"endOfLine": "lf",
"vueIndentScriptAndStyle": false
}
eslint.config.js (핵심 규칙)
// Flat config (ESLint 9.x)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
{ ignores: ['dist/', 'node_modules/'] },
js.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: { parserOptions: { parser: tseslint.parser } },
},
{
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
eslintConfigPrettier,
);
10. 멀티모듈 연동 (Gradle 루트 + Vite)
최종 프로젝트 트리
WBX-Spring/
├── settings.gradle # include 'wbx-spring-core', 'wtm-api'
├── build.gradle # 공통 Java 설정
│
├── wbx-spring-core/ # 프레임워크 라이브러리
├── wtm-api/ # BE — Spring Boot 앱
│
└── wtm-frontend/ # FE — Vue 3 + PrimeVue 4
├── package.json
├── vite.config.ts
└── src/
├── app/
├── core/
├── modules/
└── assets/
vite.config.ts (API 프록시)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath } from 'url';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-primevue': ['primevue'],
'vendor-chart': ['chart.js'],
},
},
},
},
});
11. 개발 흐름 (Day 1 → 안정화)
Day 1 — 스캐폴딩 (리드)
├── npm create vite@latest wtm-frontend -- --template vue-ts
├── PrimeVue 4, Pinia, Vue Router, Axios, VueUse 설치
├── core/ 인프라 구성 (axios, auth guard, AppLayout)
├── ESLint + Prettier + .editorconfig 설정
└── git push → 팀 공유
Day 2 — 모듈 껍데기 (각 개발자)
├── 담당 모듈 폴더 생성 (6파일 패턴)
├── 빈 View + routes 등록
├── service + types 스텁 작성
└── 모듈별 PR → 머지
Day 3+ — 병렬 개발 (각자 모듈 내 자유 작업)
├── 풀스택 ① → project/ → wbs/ → teal/ → report/
├── 풀스택 ② → timesheet/ → approval/
├── 풀스택 ③ → auth/ → dashboard/ → user/
└── 리드 → core/ 개선, 코드 리뷰, BaseCrudTable 추출
W8 — 통합
├── 모듈 간 연동 테스트 (시수 → 결재 → 알림)
├── 반응형/접근성 검수
└── 번들 사이즈 최적화
12. 네이밍 컨벤션
| 항목 | 규칙 | 예시 |
|---|---|---|
| Vue 컴포넌트 파일 | PascalCase | TimesheetGrid.vue |
| View 파일 | PascalCase + View 접미사 |
TimesheetWeekView.vue |
| TS 파일 | kebab-case 또는 dot notation | timesheet.service.ts |
| Pinia 스토어 | use{Module}Store |
useTimesheetStore |
| 라우트 name | kebab-case | timesheet-week |
| CSS 클래스 | kebab-case (BEM 선택) | timesheet-grid__cell |
| API 서비스 함수 | camelCase, CRUD 동사 | getEntries, createEntry |
| 타입/인터페이스 | PascalCase | TimesheetEntry |
| Enum/상수 | UPPER_SNAKE_CASE | TIMESHEET_STATUS |