파일
wbx-spring/plans/wtmgr/13-frontend-setup-plan.md
accura0117 9707a6eeb1 feat: FE 화면 구현 완료 + 샘플 데이터 + 결재라인 연동
- WBS/TEAL 화면 실제 구현 (TreeTable, FileUpload, 버전관리)
- 시수이력/결재이력 화면 구현 (DataTable, Filter, Timeline)
- 비밀번호변경 화면 추가
- 로그인 snake_case 응답 매핑 수정
- Vite 프록시 8081 포트 수정
- auth guard에서 fetchMe 자동 호출
- V108 샘플 데이터 (10명 사용자, 4주 시수 215건, 결재 9건)
- 배너 추가 (WBX Spring)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:17:32 +09:00

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